diff --git a/.aoneci/workflows/ci.yml b/.aoneci/workflows/ci.yml
new file mode 100644
index 00000000..7a3296a0
--- /dev/null
+++ b/.aoneci/workflows/ci.yml
@@ -0,0 +1,69 @@
+# .aoneci/workflows/ci.yml
+
+name: Qwen Code CI
+
+triggers:
+ push:
+ branches: [main, dev, integration]
+ merge_request:
+
+jobs:
+ build:
+ name: Build and Lint
+ steps:
+ - uses: checkout
+ - uses: setup-env
+ inputs:
+ node-version: '20'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run formatter check
+ run: |
+ npm run format
+ git diff --exit-code
+
+ - name: Run linter
+ run: npm run lint:ci
+
+ - name: Build project
+ run: npm run build
+
+ - name: Run type check
+ run: npm run typecheck
+
+ - name: Upload build artifacts
+ uses: upload-artifact
+ inputs:
+ name: build-artifacts-20
+ path: |
+ packages/*/dist/**/*
+ package-lock.json
+
+ test:
+ name: Test
+ needs: build # This job depends on the 'build' job
+ steps:
+ - uses: checkout
+
+ - uses: setup-env
+ inputs:
+ node-version: '20'
+
+ - uses: download-artifact
+ inputs:
+ name: build-artifacts-20
+ path: .
+
+ - name: Install dependencies for testing
+ run: npm ci
+
+ - name: Run tests and generate reports
+ run: NO_COLOR=true npm run test:ci
+
+ - name: Upload coverage reports
+ uses: upload-artifact
+ inputs:
+ name: coverage-reports-20
+ path: packages/*/coverage
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..0d6a1a36
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*]
+charset = utf-8
+insert_final_newline = true
+end_of_line = lf
+indent_style = space
+indent_size = 2
+max_line_length = 80
+
+[Makefile]
+indent_style = tab
+indent_size = 8
diff --git a/.gcp/Dockerfile.gemini-code-builder b/.gcp/Dockerfile.gemini-code-builder
new file mode 100644
index 00000000..94499edd
--- /dev/null
+++ b/.gcp/Dockerfile.gemini-code-builder
@@ -0,0 +1,89 @@
+# Use a common base image like Debian.
+# Using 'bookworm-slim' for a balance of size and compatibility.
+FROM debian:bookworm-slim
+
+# Set environment variables to prevent interactive prompts during installation
+ENV DEBIAN_FRONTEND=noninteractive
+ENV NODE_VERSION=20.12.2
+ENV NODE_VERSION_MAJOR=20
+ENV DOCKER_CLI_VERSION=26.1.3
+ENV BUILDX_VERSION=v0.14.0
+
+# Install dependencies for adding NodeSource repository, gcloud, and other tools
+# - curl: for downloading files
+# - gnupg: for managing GPG keys (used by NodeSource & Google Cloud SDK)
+# - apt-transport-https: for HTTPS apt repositories
+# - ca-certificates: for HTTPS apt repositories
+# - rsync: the rsync utility itself
+# - git: often useful in build environments
+# - python3, python3-pip, python3-venv, python3-crcmod: for gcloud SDK and some of its components
+# - lsb-release: for gcloud install script to identify distribution
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ curl \
+ gnupg \
+ apt-transport-https \
+ ca-certificates \
+ rsync \
+ git \
+ python3 \
+ python3-pip \
+ python3-venv \
+ python3-crcmod \
+ lsb-release \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install Node.js and npm
+# We'll use the official NodeSource repository for a specific version
+RUN set -eux; \
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
+ # For Node.js 20.x, it's node_20.x
+ # Let's explicitly define the major version for clarity
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
+ apt-get update && \
+ apt-get install -y --no-install-recommends nodejs && \
+ npm install -g npm@latest && \
+ # Verify installations
+ node -v && \
+ npm -v && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install Docker CLI
+# Download the static binary from Docker's official source
+RUN set -eux; \
+ DOCKER_CLI_ARCH=$(dpkg --print-architecture); \
+ case "${DOCKER_CLI_ARCH}" in \
+ amd64) DOCKER_CLI_ARCH_SUFFIX="x86_64" ;; \
+ arm64) DOCKER_CLI_ARCH_SUFFIX="aarch64" ;; \
+ *) echo "Unsupported architecture: ${DOCKER_CLI_ARCH}"; exit 1 ;; \
+ esac; \
+ curl -fsSL "https://download.docker.com/linux/static/stable/${DOCKER_CLI_ARCH_SUFFIX}/docker-${DOCKER_CLI_VERSION}.tgz" -o docker.tgz && \
+ tar -xzf docker.tgz --strip-components=1 -C /usr/local/bin docker/docker && \
+ rm docker.tgz && \
+ # Verify installation
+ docker --version
+
+# Install Docker Buildx plugin
+RUN set -eux; \
+ BUILDX_ARCH_DEB=$(dpkg --print-architecture); \
+ case "${BUILDX_ARCH_DEB}" in \
+ amd64) BUILDX_ARCH_SUFFIX="amd64" ;; \
+ arm64) BUILDX_ARCH_SUFFIX="arm64" ;; \
+ *) echo "Unsupported architecture for Buildx: ${BUILDX_ARCH_DEB}"; exit 1 ;; \
+ esac; \
+ mkdir -p /usr/local/lib/docker/cli-plugins && \
+ curl -fsSL "https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-${BUILDX_ARCH_SUFFIX}" -o /usr/local/lib/docker/cli-plugins/docker-buildx && \
+ chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx && \
+ # verify installation
+ docker buildx version
+
+# Install Google Cloud SDK (gcloud CLI)
+RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg && apt-get update -y && apt-get install google-cloud-cli -y
+
+# Set a working directory (optional, but good practice)
+WORKDIR /workspace
+
+# You can add a CMD or ENTRYPOINT if you intend to run this image directly,
+# but for Cloud Build, it's usually not necessary as Cloud Build steps override it.
+# For example:
+ENTRYPOINT '/bin/bash'
\ No newline at end of file
diff --git a/.gcp/release-docker.yaml b/.gcp/release-docker.yaml
new file mode 100644
index 00000000..a3bd7e70
--- /dev/null
+++ b/.gcp/release-docker.yaml
@@ -0,0 +1,75 @@
+steps:
+ # Step 1: Install root dependencies (includes workspaces)
+ - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
+ id: 'Install Dependencies'
+ entrypoint: 'npm'
+ args: ['install']
+
+ # Step 4: Authenticate for Docker (so we can push images to the artifact registry)
+ - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
+ id: 'Authenticate docker'
+ entrypoint: 'npm'
+ args: ['run', 'auth']
+
+ # Step 5: Build workspace packages
+ - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
+ id: 'Build packages'
+ entrypoint: 'npm'
+ args: ['run', 'build:packages']
+
+ # Step 6: Determine Docker Image Tag
+ - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
+ id: 'Determine Docker Image Tag'
+ entrypoint: 'bash'
+ args:
+ - -c
+ - |
+ SHELL_TAG_NAME="$TAG_NAME"
+ FINAL_TAG="$SHORT_SHA" # Default to SHA
+ if [[ "$$SHELL_TAG_NAME" == *"-nightly"* ]]; then
+ echo "Nightly release detected."
+ FINAL_TAG="$${SHELL_TAG_NAME#v}"
+ # Also escape the variable in the regex match
+ elif [[ "$$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Official release detected."
+ FINAL_TAG="$${SHELL_TAG_NAME#v}"
+ else
+ echo "Development/RC release detected. Using commit SHA as tag."
+ fi
+ echo "Determined image tag: $$FINAL_TAG"
+ echo "$$FINAL_TAG" > /workspace/image_tag.txt
+
+ # Step 7: Build sandbox container image
+ - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
+ id: 'Build sandbox Docker image'
+ entrypoint: 'bash'
+ args:
+ - -c
+ - |
+ export GEMINI_SANDBOX_IMAGE_TAG=$$(cat /workspace/image_tag.txt)
+ echo "Using Docker image tag for build: $$GEMINI_SANDBOX_IMAGE_TAG"
+ npm run build:sandbox
+ env:
+ - 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
+
+ # Step 8: Publish sandbox container image
+ - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
+ id: 'Publish sandbox Docker image'
+ entrypoint: 'bash'
+ args:
+ - -c
+ - |
+ set -e
+ FINAL_IMAGE_URI=$$(cat /workspace/final_image_uri.txt)
+
+ echo "Pushing sandbox image: $${FINAL_IMAGE_URI}"
+ $_CONTAINER_TOOL push "$${FINAL_IMAGE_URI}"
+ env:
+ - 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
+
+options:
+ defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET
+ dynamicSubstitutions: true
+
+substitutions:
+ _CONTAINER_TOOL: 'docker'
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..deab5ae8
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,24 @@
+# Set the default behavior for all files to automatically handle line endings.
+# This will ensure that all text files are normalized to use LF (line feed)
+# line endings in the repository, which helps prevent cross-platform issues.
+* text=auto eol=lf
+
+# Explicitly declare files that must have LF line endings for proper execution
+# on Unix-like systems.
+*.sh eol=lf
+*.bash eol=lf
+Makefile eol=lf
+
+# Explicitly declare binary file types to prevent Git from attempting to
+# normalize their line endings.
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.pdf binary
+*.woff binary
+*.woff2 binary
+*.eot binary
+*.ttf binary
+*.otf binary
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..bc16c551
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,7 @@
+# By default, require reviews from the release approvers for all files.
+* @google-gemini/gemini-cli-askmode-approvers
+
+# The following files don't need reviews from the release approvers.
+# These patterns override the rule above.
+**/*.md
+/docs/
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..12bf4663
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,55 @@
+name: Bug Report
+description: Report a bug to help us improve Gemini CLI
+labels: ['kind/bug', 'status/need-triage']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ > [!IMPORTANT]
+ > Thanks for taking the time to fill out this bug report!
+ >
+ > Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if an issue already exists for the bug you encountered.
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: What happened?
+ description: A clear and concise description of what the bug is.
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: What did you expect to happen?
+ validations:
+ required: true
+
+ - type: textarea
+ id: info
+ attributes:
+ label: Client information
+ description: Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (MacOS, Windows, Linux).
+ value: |
+
+
+ ```console
+ $ gemini /about
+ # paste output here
+ ```
+
+
+ validations:
+ required: true
+
+ - type: textarea
+ id: login-info
+ attributes:
+ label: Login information
+ description: Describe how you are logging in (e.g., Google Account, API key).
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Anything else we need to know?
+ description: Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 00000000..c08de46a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,33 @@
+name: Feature Request
+description: Suggest an idea for this project
+labels: ['kind/enhancement', 'status/need-triage']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ > [!IMPORTANT]
+ > Thanks for taking the time to suggest an enhancement!
+ >
+ > Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if a similar feature has already been requested.
+
+ - type: textarea
+ id: feature
+ attributes:
+ label: What would you like to be added?
+ description: A clear and concise description of the enhancement.
+ validations:
+ required: true
+
+ - type: textarea
+ id: rationale
+ attributes:
+ label: Why is this needed?
+ description: A clear and concise description of why this enhancement is needed.
+ validations:
+ required: true
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional context
+ description: Add any other context or screenshots about the feature request here.
diff --git a/.github/actions/post-coverage-comment/action.yml b/.github/actions/post-coverage-comment/action.yml
new file mode 100644
index 00000000..20b67019
--- /dev/null
+++ b/.github/actions/post-coverage-comment/action.yml
@@ -0,0 +1,102 @@
+name: 'Post Coverage Comment Action'
+description: 'Prepares and posts a code coverage comment to a PR.'
+
+inputs:
+ cli_json_file:
+ description: 'Path to CLI coverage-summary.json'
+ required: true
+ core_json_file:
+ description: 'Path to Core coverage-summary.json'
+ required: true
+ cli_full_text_summary_file:
+ description: 'Path to CLI full-text-summary.txt'
+ required: true
+ core_full_text_summary_file:
+ description: 'Path to Core full-text-summary.txt'
+ required: true
+ node_version:
+ description: 'Node.js version for context in messages'
+ required: true
+ github_token:
+ description: 'GitHub token for posting comments'
+ required: true
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Prepare Coverage Comment
+ id: prep_coverage_comment
+ shell: bash
+ run: |
+ cli_json_file="${{ inputs.cli_json_file }}"
+ core_json_file="${{ inputs.core_json_file }}"
+ cli_full_text_summary_file="${{ inputs.cli_full_text_summary_file }}"
+ core_full_text_summary_file="${{ inputs.core_full_text_summary_file }}"
+ comment_file="coverage-comment.md"
+
+ # Extract percentages using jq for the main table
+ if [ -f "$cli_json_file" ]; then
+ cli_lines_pct=$(jq -r '.total.lines.pct' "$cli_json_file")
+ cli_statements_pct=$(jq -r '.total.statements.pct' "$cli_json_file")
+ cli_functions_pct=$(jq -r '.total.functions.pct' "$cli_json_file")
+ cli_branches_pct=$(jq -r '.total.branches.pct' "$cli_json_file")
+ else
+ cli_lines_pct="N/A"; cli_statements_pct="N/A"; cli_functions_pct="N/A"; cli_branches_pct="N/A"
+ echo "CLI coverage-summary.json not found at: $cli_json_file" >&2 # Error to stderr
+ fi
+
+ if [ -f "$core_json_file" ]; then
+ core_lines_pct=$(jq -r '.total.lines.pct' "$core_json_file")
+ core_statements_pct=$(jq -r '.total.statements.pct' "$core_json_file")
+ core_functions_pct=$(jq -r '.total.functions.pct' "$core_json_file")
+ core_branches_pct=$(jq -r '.total.branches.pct' "$core_json_file")
+ else
+ core_lines_pct="N/A"; core_statements_pct="N/A"; core_functions_pct="N/A"; core_branches_pct="N/A"
+ echo "Core coverage-summary.json not found at: $core_json_file" >&2 # Error to stderr
+ fi
+
+ echo "## Code Coverage Summary" > "$comment_file"
+ echo "" >> "$comment_file"
+ echo "| Package | Lines | Statements | Functions | Branches |" >> "$comment_file"
+ echo "|---|---|---|---|---|" >> "$comment_file"
+ echo "| CLI | ${cli_lines_pct}% | ${cli_statements_pct}% | ${cli_functions_pct}% | ${cli_branches_pct}% |" >> "$comment_file"
+ echo "| Core | ${core_lines_pct}% | ${core_statements_pct}% | ${core_functions_pct}% | ${core_branches_pct}% |" >> "$comment_file"
+ echo "" >> "$comment_file"
+
+ # CLI Package - Collapsible Section (with full text summary from file)
+ echo "" >> "$comment_file"
+ echo "CLI Package - Full Text Report
" >> "$comment_file"
+ echo "" >> "$comment_file"
+ echo '```text' >> "$comment_file"
+ if [ -f "$cli_full_text_summary_file" ]; then
+ cat "$cli_full_text_summary_file" >> "$comment_file"
+ else
+ echo "CLI full-text-summary.txt not found at: $cli_full_text_summary_file" >> "$comment_file"
+ fi
+ echo '```' >> "$comment_file"
+ echo " " >> "$comment_file"
+ echo "" >> "$comment_file"
+
+ # Core Package - Collapsible Section (with full text summary from file)
+ echo "" >> "$comment_file"
+ echo "Core Package - Full Text Report
" >> "$comment_file"
+ echo "" >> "$comment_file"
+ echo '```text' >> "$comment_file"
+ if [ -f "$core_full_text_summary_file" ]; then
+ cat "$core_full_text_summary_file" >> "$comment_file"
+ else
+ echo "Core full-text-summary.txt not found at: $core_full_text_summary_file" >> "$comment_file"
+ fi
+ echo '```' >> "$comment_file"
+ echo " " >> "$comment_file"
+ echo "" >> "$comment_file"
+
+ echo "_For detailed HTML reports, please see the 'coverage-reports-${{ inputs.node_version }}' artifact from the main CI run._" >> "$comment_file"
+
+ - name: Post Coverage Comment
+ uses: thollander/actions-comment-pull-request@v3
+ if: always()
+ with:
+ file-path: coverage-comment.md # Use the generated file directly
+ comment-tag: code-coverage-summary
+ github-token: ${{ inputs.github_token }}
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..560eec25
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,41 @@
+## TLDR
+
+
+
+## Dive Deeper
+
+
+
+## Reviewer Test Plan
+
+
+
+## Testing Matrix
+
+
+
+| | ๐ | ๐ช | ๐ง |
+| -------- | --- | --- | --- |
+| npm run | โ | โ | โ |
+| npx | โ | โ | โ |
+| Docker | โ | โ | โ |
+| Podman | โ | - | - |
+| Seatbelt | โ | - | - |
+
+## Linked issues / bugs
+
+
diff --git a/.github/scripts/pr-triage.sh b/.github/scripts/pr-triage.sh
new file mode 100755
index 00000000..be86e393
--- /dev/null
+++ b/.github/scripts/pr-triage.sh
@@ -0,0 +1,163 @@
+#!/bin/bash
+set -euo pipefail
+
+# Initialize a comma-separated string to hold PR numbers that need a comment
+PRS_NEEDING_COMMENT=""
+
+# Function to process a single PR
+process_pr() {
+ local PR_NUMBER=$1
+ echo "๐ Processing PR #$PR_NUMBER"
+
+ # Get PR body with error handling
+ local PR_BODY
+ if ! PR_BODY=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json body -q .body 2>/dev/null); then
+ echo " โ ๏ธ Could not fetch PR #$PR_NUMBER details"
+ return 1
+ fi
+
+ # Look for issue references using multiple patterns
+ local ISSUE_NUMBER=""
+
+ # Pattern 1: Direct reference like #123
+ if [ -z "$ISSUE_NUMBER" ]; then
+ ISSUE_NUMBER=$(echo "$PR_BODY" | grep -oE '#[0-9]+' | head -1 | sed 's/#//' 2>/dev/null || echo "")
+ fi
+
+ # Pattern 2: Closes/Fixes/Resolves patterns (case insensitive)
+ if [ -z "$ISSUE_NUMBER" ]; then
+ ISSUE_NUMBER=$(echo "$PR_BODY" | grep -iE '(closes?|fixes?|resolves?) #[0-9]+' | grep -oE '#[0-9]+' | head -1 | sed 's/#//' 2>/dev/null || echo "")
+ fi
+
+ if [ -z "$ISSUE_NUMBER" ]; then
+ echo "โ ๏ธ No linked issue found for PR #$PR_NUMBER, adding status/need-issue label"
+ if ! gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --add-label "status/need-issue" 2>/dev/null; then
+ echo " โ ๏ธ Failed to add label (may already exist or have permission issues)"
+ fi
+ # Add PR number to the list
+ if [ -z "$PRS_NEEDING_COMMENT" ]; then
+ PRS_NEEDING_COMMENT="$PR_NUMBER"
+ else
+ PRS_NEEDING_COMMENT="$PRS_NEEDING_COMMENT,$PR_NUMBER"
+ fi
+ echo "needs_comment=true" >> $GITHUB_OUTPUT
+ else
+ echo "๐ Found linked issue #$ISSUE_NUMBER"
+
+ # Remove status/need-issue label if present
+ if ! gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --remove-label "status/need-issue" 2>/dev/null; then
+ echo " status/need-issue label not present or could not be removed"
+ fi
+
+ # Get issue labels
+ echo "๐ฅ Fetching labels from issue #$ISSUE_NUMBER"
+ local ISSUE_LABELS=""
+ if ! ISSUE_LABELS=$(gh issue view "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --json labels -q '.labels[].name' 2>/dev/null | tr '\n' ',' | sed 's/,$//' || echo ""); then
+ echo " โ ๏ธ Could not fetch issue #$ISSUE_NUMBER (may not exist or be in different repo)"
+ ISSUE_LABELS=""
+ fi
+
+ # Get PR labels
+ echo "๐ฅ Fetching labels from PR #$PR_NUMBER"
+ local PR_LABELS=""
+ if ! PR_LABELS=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json labels -q '.labels[].name' 2>/dev/null | tr '\n' ',' | sed 's/,$//' || echo ""); then
+ echo " โ ๏ธ Could not fetch PR labels"
+ PR_LABELS=""
+ fi
+
+ echo " Issue labels: $ISSUE_LABELS"
+ echo " PR labels: $PR_LABELS"
+
+ # Convert comma-separated strings to arrays
+ local ISSUE_LABEL_ARRAY PR_LABEL_ARRAY
+ IFS=',' read -ra ISSUE_LABEL_ARRAY <<< "$ISSUE_LABELS"
+ IFS=',' read -ra PR_LABEL_ARRAY <<< "$PR_LABELS"
+
+ # Find labels to add (on issue but not on PR)
+ local LABELS_TO_ADD=""
+ for label in "${ISSUE_LABEL_ARRAY[@]}"; do
+ if [ -n "$label" ] && [[ ! " ${PR_LABEL_ARRAY[*]} " =~ " ${label} " ]]; then
+ if [ -z "$LABELS_TO_ADD" ]; then
+ LABELS_TO_ADD="$label"
+ else
+ LABELS_TO_ADD="$LABELS_TO_ADD,$label"
+ fi
+ fi
+ done
+
+ # Find labels to remove (on PR but not on issue)
+ local LABELS_TO_REMOVE=""
+ for label in "${PR_LABEL_ARRAY[@]}"; do
+ if [ -n "$label" ] && [[ ! " ${ISSUE_LABEL_ARRAY[*]} " =~ " ${label} " ]]; then
+ # Don't remove status/need-issue since we already handled it
+ if [ "$label" != "status/need-issue" ]; then
+ if [ -z "$LABELS_TO_REMOVE" ]; then
+ LABELS_TO_REMOVE="$label"
+ else
+ LABELS_TO_REMOVE="$LABELS_TO_REMOVE,$label"
+ fi
+ fi
+ fi
+ done
+
+ # Apply label changes
+ if [ -n "$LABELS_TO_ADD" ]; then
+ echo "โ Adding labels: $LABELS_TO_ADD"
+ if ! gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --add-label "$LABELS_TO_ADD" 2>/dev/null; then
+ echo " โ ๏ธ Failed to add some labels"
+ fi
+ fi
+
+ if [ -n "$LABELS_TO_REMOVE" ]; then
+ echo "โ Removing labels: $LABELS_TO_REMOVE"
+ if ! gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --remove-label "$LABELS_TO_REMOVE" 2>/dev/null; then
+ echo " โ ๏ธ Failed to remove some labels"
+ fi
+ fi
+
+ if [ -z "$LABELS_TO_ADD" ] && [ -z "$LABELS_TO_REMOVE" ]; then
+ echo "โ
Labels already synchronized"
+ fi
+ echo "needs_comment=false" >> $GITHUB_OUTPUT
+ fi
+}
+
+# If PR_NUMBER is set, process only that PR
+if [ -n "${PR_NUMBER:-}" ]; then
+ if ! process_pr "$PR_NUMBER"; then
+ echo "โ Failed to process PR #$PR_NUMBER"
+ exit 1
+ fi
+else
+ # Otherwise, get all open PRs and process them
+ # The script logic will determine which ones need issue linking or label sync
+ echo "๐ฅ Getting all open pull requests..."
+ if ! PR_NUMBERS=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --limit 1000 --json number -q '.[].number' 2>/dev/null); then
+ echo "โ Failed to fetch PR list"
+ exit 1
+ fi
+
+ if [ -z "$PR_NUMBERS" ]; then
+ echo "โ
No open PRs found"
+ else
+ # Count the number of PRs
+ PR_COUNT=$(echo "$PR_NUMBERS" | wc -w | tr -d ' ')
+ echo "๐ Found $PR_COUNT open PRs to process"
+
+ for pr_number in $PR_NUMBERS; do
+ if ! process_pr "$pr_number"; then
+ echo "โ ๏ธ Failed to process PR #$pr_number, continuing with next PR..."
+ continue
+ fi
+ done
+ fi
+fi
+
+# Ensure output is always set, even if empty
+if [ -z "$PRS_NEEDING_COMMENT" ]; then
+ echo "prs_needing_comment=[]" >> $GITHUB_OUTPUT
+else
+ echo "prs_needing_comment=[$PRS_NEEDING_COMMENT]" >> $GITHUB_OUTPUT
+fi
+
+echo "โ
PR triage completed"
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..2ef43ed5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,165 @@
+# .github/workflows/ci.yml
+
+name: Gemini CLI CI
+
+on:
+ push:
+ branches: [main, release]
+ pull_request:
+ branches: [main, release]
+ merge_group:
+
+jobs:
+ build:
+ name: Build and Lint
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read # For checkout
+ strategy:
+ matrix:
+ node-version: [20.x, 22.x, 24.x]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - name: Set up Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run formatter check
+ run: |
+ npm run format
+ git diff --exit-code
+
+ - name: Run linter
+ run: npm run lint:ci
+
+ - name: Build project
+ run: npm run build
+
+ - name: Run type check
+ run: npm run typecheck
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: build-artifacts-${{ matrix.node-version }}
+ path: |
+ packages/*/dist
+ package-lock.json # Only upload dist and lockfile
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ needs: build # This job depends on the 'build' job
+ permissions:
+ contents: read
+ checks: write
+ pull-requests: write
+ strategy:
+ matrix:
+ node-version: [20.x, 22.x, 24.x] # Should match the build job's matrix
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - name: Set up Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'npm'
+
+ - name: Download build artifacts
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ with:
+ name: build-artifacts-${{ matrix.node-version }}
+ path: . # Download to the root, this will include package-lock.json and packages/*/dist
+
+ # Restore/create package structure for dist folders if necessary.
+ # The download-artifact action with path: . should place them correctly if the
+ # upload paths were relative to the workspace root.
+ # Example: if uploaded `packages/cli/dist`, it will be at `./packages/cli/dist`.
+
+ - name: Install dependencies for testing
+ run: npm ci # Install fresh dependencies using the downloaded package-lock.json
+
+ - name: Run tests and generate reports
+ run: NO_COLOR=true npm run test:ci
+
+ - name: Publish Test Report (for non-forks)
+ if: always() && (github.event.pull_request.head.repo.full_name == github.repository)
+ uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2
+ with:
+ name: Test Results (Node ${{ matrix.node-version }})
+ path: packages/*/junit.xml
+ reporter: java-junit
+ fail-on-error: 'false'
+
+ - name: Upload Test Results Artifact (for forks)
+ if: always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: test-results-fork-${{ matrix.node-version }}
+ path: packages/*/junit.xml
+
+ - name: Upload coverage reports
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ if: always()
+ with:
+ name: coverage-reports-${{ matrix.node-version }}
+ path: packages/*/coverage
+
+ post_coverage_comment:
+ name: Post Coverage Comment
+ runs-on: ubuntu-latest
+ needs: test
+ if: always() && github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name == github.repository)
+ continue-on-error: true
+ permissions:
+ contents: read # For checkout
+ pull-requests: write # For commenting
+ strategy:
+ matrix:
+ node-version: [22.x] # Reduce noise by only posting the comment once
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - name: Download coverage reports artifact
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ with:
+ name: coverage-reports-${{ matrix.node-version }}
+ path: coverage_artifact # Download to a specific directory
+
+ - name: Post Coverage Comment using Composite Action
+ uses: ./.github/actions/post-coverage-comment # Path to the composite action directory
+ with:
+ cli_json_file: coverage_artifact/cli/coverage/coverage-summary.json
+ core_json_file: coverage_artifact/core/coverage/coverage-summary.json
+ cli_full_text_summary_file: coverage_artifact/cli/coverage/full-text-summary.txt
+ core_full_text_summary_file: coverage_artifact/core/coverage/full-text-summary.txt
+ node_version: ${{ matrix.node-version }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ codeql:
+ name: CodeQL
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3
+ with:
+ languages: javascript
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3
diff --git a/.github/workflows/community-report.yml b/.github/workflows/community-report.yml
new file mode 100644
index 00000000..28aa2cba
--- /dev/null
+++ b/.github/workflows/community-report.yml
@@ -0,0 +1,188 @@
+name: Generate Weekly Community Report ๐
+
+on:
+ schedule:
+ - cron: '0 12 * * 1' # Run at 12:00 UTC on Monday
+ workflow_dispatch:
+ inputs:
+ days:
+ description: 'Number of days to look back for the report'
+ required: true
+ default: '7'
+
+jobs:
+ generate-report:
+ name: Generate Report ๐
+ if: ${{ github.repository == 'google-gemini/gemini-cli' }}
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: read
+ discussions: read
+ contents: read
+ id-token: write
+
+ steps:
+ - name: Generate GitHub App Token ๐
+ id: generate_token
+ uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2
+ with:
+ app-id: ${{ secrets.APP_ID }}
+ private-key: ${{ secrets.PRIVATE_KEY }}
+
+ - name: Generate Report ๐
+ id: report
+ env:
+ GH_TOKEN: ${{ steps.generate_token.outputs.token }}
+ REPO: ${{ github.repository }}
+ DAYS: ${{ github.event.inputs.days || '7' }}
+ run: |
+ set -e
+
+ START_DATE=$(date -u -d "$DAYS days ago" +'%Y-%m-%d')
+ END_DATE=$(date -u +'%Y-%m-%d')
+ echo "โณ Generating report for contributions from $START_DATE to $END_DATE..."
+
+ declare -A author_is_googler
+ check_googler_status() {
+ local author=$1
+ if [[ "$author" == *"[bot]" ]]; then
+ author_is_googler[$author]=1
+ return 1
+ fi
+ if [[ -v "author_is_googler[$author]" ]]; then
+ return ${author_is_googler[$author]}
+ fi
+
+ if gh api "orgs/googlers/members/$author" --silent 2>/dev/null; then
+ echo "๐งโ๐ป $author is a Googler."
+ author_is_googler[$author]=0
+ else
+ echo "๐ $author is a community contributor."
+ author_is_googler[$author]=1
+ fi
+ return ${author_is_googler[$author]}
+ }
+
+ googler_issues=0
+ non_googler_issues=0
+ googler_prs=0
+ non_googler_prs=0
+
+ echo "๐ Fetching issues and pull requests..."
+ ITEMS_JSON=$(gh search issues --repo "$REPO" "created:>$START_DATE" --json author,isPullRequest --limit 1000)
+
+ for row in $(echo "${ITEMS_JSON}" | jq -r '.[] | @base64'); do
+ _jq() {
+ echo ${row} | base64 --decode | jq -r ${1}
+ }
+ author=$(_jq '.author.login')
+ is_pr=$(_jq '.isPullRequest')
+
+ if [[ -z "$author" || "$author" == "null" ]]; then
+ continue
+ fi
+
+ if check_googler_status "$author"; then
+ if [[ "$is_pr" == "true" ]]; then
+ ((googler_prs++))
+ else
+ ((googler_issues++))
+ fi
+ else
+ if [[ "$is_pr" == "true" ]]; then
+ ((non_googler_prs++))
+ else
+ ((non_googler_issues++))
+ fi
+ fi
+ done
+
+ googler_discussions=0
+ non_googler_discussions=0
+
+ echo "๐ฃ๏ธ Fetching discussions..."
+ DISCUSSION_QUERY='''
+ query($q: String!) {
+ search(query: $q, type: DISCUSSION, first: 100) {
+ nodes {
+ ... on Discussion {
+ author {
+ login
+ }
+ }
+ }
+ }
+ }'''
+ DISCUSSIONS_JSON=$(gh api graphql -f q="repo:$REPO created:>$START_DATE" -f query="$DISCUSSION_QUERY")
+
+ for row in $(echo "${DISCUSSIONS_JSON}" | jq -r '.data.search.nodes[] | @base64'); do
+ _jq() {
+ echo ${row} | base64 --decode | jq -r ${1}
+ }
+ author=$(_jq '.author.login')
+
+ if [[ -z "$author" || "$author" == "null" ]]; then
+ continue
+ fi
+
+ if check_googler_status "$author"; then
+ ((googler_discussions++))
+ else
+ ((non_googler_discussions++))
+ fi
+ done
+
+ echo "โ๏ธ Generating report content..."
+ REPORT_TITLE="Community Contribution Report: $START_DATE to $END_DATE"
+ TOTAL_ISSUES=$((googler_issues + non_googler_issues))
+ TOTAL_PRS=$((googler_prs + non_googler_prs))
+ TOTAL_DISCUSSIONS=$((googler_discussions + non_googler_discussions))
+
+ REPORT_BODY=$(cat <> $GITHUB_OUTPUT
+ echo "$REPORT_BODY" >> $GITHUB_OUTPUT
+ echo "EOF" >> $GITHUB_OUTPUT
+
+ echo "๐ Community Contribution Report:"
+ echo "$REPORT_BODY"
+
+ - name: ๐ค Get Insights from Report
+ if: steps.report.outputs.report_body != ''
+ uses: google-gemini/gemini-cli-action@df3f890f003d28c60a2a09d2c29e0126e4d1e2ff
+ env:
+ GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
+ with:
+ version: 0.1.8-rc.0
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ OTLP_GCP_WIF_PROVIDER: ${{ secrets.OTLP_GCP_WIF_PROVIDER }}
+ OTLP_GOOGLE_CLOUD_PROJECT: ${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}
+ settings_json: |
+ {
+ "coreTools": [
+ "run_shell_command(gh issue list)",
+ "run_shell_command(gh pr list)",
+ "run_shell_command(gh search issues)",
+ "run_shell_command(gh search prs)"
+ ]
+ }
+ prompt: |
+ You are a helpful assistant that analyzes community contribution reports.
+ Based on the following report, please provide a brief summary and highlight any interesting trends or potential areas for improvement.
+
+ Report:
+ ${{ steps.report.outputs.report_body }}
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 00000000..745627a1
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,48 @@
+# .github/workflows/e2e.yml
+
+name: E2E Tests
+
+on:
+ push:
+ branches: [main]
+ merge_group:
+
+jobs:
+ e2e-test:
+ name: E2E Test - ${{ matrix.sandbox }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ sandbox: [sandbox:none, sandbox:docker]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: 20.x
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build project
+ run: npm run build
+
+ - name: Set up Docker
+ if: matrix.sandbox == 'sandbox:docker'
+ uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
+
+ - name: Set up Podman
+ if: matrix.sandbox == 'sandbox:podman'
+ uses: redhat-actions/podman-login@4934294ad0449894bcd1e9f191899d7292469603 # v1
+ with:
+ registry: docker.io
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Run E2E tests
+ env:
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ run: npm run test:integration:${{ matrix.sandbox }} -- --verbose --keep-output
diff --git a/.github/workflows/gemini-automated-issue-triage.yml b/.github/workflows/gemini-automated-issue-triage.yml
new file mode 100644
index 00000000..ed465980
--- /dev/null
+++ b/.github/workflows/gemini-automated-issue-triage.yml
@@ -0,0 +1,63 @@
+name: Gemini Automated Issue Triage
+
+on:
+ issues:
+ types: [opened, reopened]
+
+jobs:
+ triage-issue:
+ timeout-minutes: 5
+ if: ${{ github.repository == 'google-gemini/gemini-cli' }}
+ permissions:
+ issues: write
+ contents: read
+ id-token: write
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.event.issue.number }}
+ cancel-in-progress: true
+ runs-on: ubuntu-latest
+ steps:
+ - name: Generate GitHub App Token
+ id: generate_token
+ uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2
+ with:
+ app-id: ${{ secrets.APP_ID }}
+ private-key: ${{ secrets.PRIVATE_KEY }}
+
+ - name: Run Gemini Issue Triage
+ uses: google-gemini/gemini-cli-action@df3f890f003d28c60a2a09d2c29e0126e4d1e2ff
+ env:
+ GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
+ with:
+ version: 0.1.8-rc.0
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ OTLP_GCP_WIF_PROVIDER: ${{ secrets.OTLP_GCP_WIF_PROVIDER }}
+ OTLP_GOOGLE_CLOUD_PROJECT: ${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}
+ settings_json: |
+ {
+ "coreTools": [
+ "run_shell_command(gh label list)",
+ "run_shell_command(gh issue edit)",
+ "run_shell_command(gh issue list)"
+ ],
+ "telemetry": {
+ "enabled": true,
+ "target": "gcp"
+ },
+ "sandbox": false
+ }
+ prompt: |
+ You are an issue triage assistant. Analyze the current GitHub issue and apply the most appropriate existing labels.
+
+ Steps:
+ 1. Run: `gh label list --repo ${{ github.repository }} --limit 100` to get all available labels.
+ 2. Review the issue title and body provided in the environment variables.
+ 3. Select the most relevant labels from the existing labels, focusing on kind/*, area/*, and priority/*.
+ 4. Apply the selected labels to this issue using: `gh issue edit ${{ github.event.issue.number }} --repo ${{ github.repository }} --add-label "label1,label2"`
+ 5. If the issue has a "status/need-triage" label, remove it after applying the appropriate labels: `gh issue edit ${{ github.event.issue.number }} --repo ${{ github.repository }} --remove-label "status/need-triage"`
+
+ Guidelines:
+ - Only use labels that already exist in the repository.
+ - Do not add comments or modify the issue content.
+ - Triage only the current issue.
+ - Assign all applicable kind/*, area/*, and priority/* labels based on the issue content.
diff --git a/.github/workflows/gemini-scheduled-issue-triage.yml b/.github/workflows/gemini-scheduled-issue-triage.yml
new file mode 100644
index 00000000..781ae373
--- /dev/null
+++ b/.github/workflows/gemini-scheduled-issue-triage.yml
@@ -0,0 +1,100 @@
+name: Gemini Scheduled Issue Triage
+
+on:
+ schedule:
+ - cron: '0 * * * *' # Runs every hour
+ workflow_dispatch: {}
+
+jobs:
+ triage-issues:
+ timeout-minutes: 10
+ if: ${{ github.repository == 'google-gemini/gemini-cli' }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
+ issues: write
+ steps:
+ - name: Generate GitHub App Token
+ id: generate_token
+ uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2
+ with:
+ app-id: ${{ secrets.APP_ID }}
+ private-key: ${{ secrets.PRIVATE_KEY }}
+
+ - name: Find untriaged issues
+ id: find_issues
+ env:
+ GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
+ run: |
+ echo "๐ Finding issues without labels..."
+ NO_LABEL_ISSUES=$(gh issue list --repo ${{ github.repository }} --search "is:open is:issue no:label" --json number,title,body)
+
+ echo "๐ท๏ธ Finding issues that need triage..."
+ NEED_TRIAGE_ISSUES=$(gh issue list --repo ${{ github.repository }} --search "is:open is:issue label:\"status/need-triage\"" --json number,title,body)
+
+ echo "๐ Merging and deduplicating issues..."
+ ISSUES=$(echo "$NO_LABEL_ISSUES" "$NEED_TRIAGE_ISSUES" | jq -c -s 'add | unique_by(.number)')
+
+ echo "๐ Setting output for GitHub Actions..."
+ echo "issues_to_triage=$ISSUES" >> "$GITHUB_OUTPUT"
+
+ echo "โ
Found $(echo "$ISSUES" | jq 'length') issues to triage! ๐ฏ"
+
+ - name: Run Gemini Issue Triage
+ if: steps.find_issues.outputs.issues_to_triage != '[]'
+ uses: google-gemini/gemini-cli-action@df3f890f003d28c60a2a09d2c29e0126e4d1e2ff
+ env:
+ GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
+ ISSUES_TO_TRIAGE: ${{ steps.find_issues.outputs.issues_to_triage }}
+ REPOSITORY: ${{ github.repository }}
+ with:
+ version: 0.1.8-rc.0
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ OTLP_GCP_WIF_PROVIDER: ${{ secrets.OTLP_GCP_WIF_PROVIDER }}
+ OTLP_GOOGLE_CLOUD_PROJECT: ${{ secrets.OTLP_GOOGLE_CLOUD_PROJECT }}
+ settings_json: |
+ {
+ "coreTools": [
+ "run_shell_command(echo)",
+ "run_shell_command(gh label list)",
+ "run_shell_command(gh issue edit)",
+ "run_shell_command(gh issue list)"
+ ],
+ "telemetry": {
+ "enabled": true,
+ "target": "gcp"
+ },
+ "sandbox": false
+ }
+ prompt: |
+ You are an issue triage assistant. Analyze issues and apply appropriate labels ONE AT A TIME.
+
+ Repository: ${{ github.repository }}
+
+ Steps:
+ 1. Run: `gh label list --repo ${{ github.repository }} --limit 100` to see available labels
+ 2. Check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues)
+ 3. Parse the JSON array from step 2 and for EACH INDIVIDUAL issue, apply appropriate labels using separate commands:
+ - `gh issue edit ISSUE_NUMBER --repo ${{ github.repository }} --add-label "label1"`
+ - `gh issue edit ISSUE_NUMBER --repo ${{ github.repository }} --add-label "label2"`
+ - Continue for each label separately
+
+ IMPORTANT: Label each issue individually, one command per issue, one label at a time if needed.
+
+ Guidelines:
+ - Only use existing repository labels from step 1
+ - Do not add comments to issues
+ - Triage each issue independently based on title and body content
+ - Focus on applying: kind/* (bug/enhancement/documentation), area/* (core/cli/testing/windows), and priority/* labels
+ - If an issue has insufficient information, consider applying "status/need-information"
+ - After applying appropriate labels to an issue, remove the "status/need-triage" label if present: `gh issue edit ISSUE_NUMBER --repo ${{ github.repository }} --remove-label "status/need-triage"`
+ - Execute one `gh issue edit` command per issue, wait for success before proceeding to the next
+
+ Example triage logic:
+ - Issues with "bug", "error", "broken" โ kind/bug
+ - Issues with "feature", "enhancement", "improve" โ kind/enhancement
+ - Issues about Windows/performance โ area/windows, area/performance
+ - Critical bugs โ priority/p0, other bugs โ priority/p1, enhancements โ priority/p2
+
+ Process each issue sequentially and confirm each labeling operation before moving to the next issue.
diff --git a/.github/workflows/gemini-scheduled-pr-triage.yml b/.github/workflows/gemini-scheduled-pr-triage.yml
new file mode 100644
index 00000000..dc2228bc
--- /dev/null
+++ b/.github/workflows/gemini-scheduled-pr-triage.yml
@@ -0,0 +1,36 @@
+name: Gemini Scheduled PR Triage ๐
+
+on:
+ schedule:
+ - cron: '*/15 * * * *' # Runs every 15 minutes
+ workflow_dispatch: {}
+
+jobs:
+ audit-prs:
+ timeout-minutes: 15
+ if: ${{ github.repository == 'google-gemini/gemini-cli' }}
+ permissions:
+ contents: read
+ id-token: write
+ issues: write
+ pull-requests: write
+ runs-on: ubuntu-latest
+ outputs:
+ prs_needing_comment: ${{ steps.run_triage.outputs.prs_needing_comment }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - name: Generate GitHub App Token
+ id: generate_token
+ uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2
+ with:
+ app-id: ${{ secrets.APP_ID }}
+ private-key: ${{ secrets.PRIVATE_KEY }}
+
+ - name: Run PR Triage Script
+ id: run_triage
+ env:
+ GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ run: ./.github/scripts/pr-triage.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..9c449702
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,173 @@
+name: Release
+
+on:
+ schedule:
+ # Runs every day at midnight UTC for the nightly release.
+ - cron: '0 0 * * *'
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'The version to release (e.g., v0.1.11). Required for manual patch releases.'
+ required: false # Not required for scheduled runs
+ type: string
+ ref:
+ description: 'The branch or ref (full git sha) to release from.'
+ required: true
+ type: string
+ default: 'main'
+ dry_run:
+ description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
+ required: true
+ type: boolean
+ default: true
+ create_nightly_release:
+ description: 'Auto apply the nightly release tag, input version is ignored.'
+ required: false
+ type: boolean
+ default: false
+ force_skip_tests:
+ description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
+ required: false
+ type: boolean
+ default: false
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ environment:
+ name: production-release
+ url: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}
+ if: github.repository == 'google-gemini/gemini-cli'
+ permissions:
+ contents: write
+ packages: write
+ id-token: write
+ issues: write # For creating issues on failure
+ outputs:
+ RELEASE_TAG: ${{ steps.version.outputs.RELEASE_TAG }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: 0
+
+ - name: Set booleans for simplified logic
+ id: vars
+ run: |
+ is_nightly="false"
+ if [[ "${{ github.event_name }}" == "schedule" || "${{ github.event.inputs.create_nightly_release }}" == "true" ]]; then
+ is_nightly="true"
+ fi
+ echo "is_nightly=${is_nightly}" >> $GITHUB_OUTPUT
+
+ is_dry_run="false"
+ if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then
+ is_dry_run="true"
+ fi
+ echo "is_dry_run=${is_dry_run}" >> $GITHUB_OUTPUT
+
+ - name: Setup Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+
+ - name: Install Dependencies
+ run: npm ci
+
+ - name: Get the version
+ id: version
+ run: |
+ VERSION_JSON=$(node scripts/get-release-version.js)
+ echo "RELEASE_TAG=$(echo $VERSION_JSON | jq -r .releaseTag)" >> $GITHUB_OUTPUT
+ echo "RELEASE_VERSION=$(echo $VERSION_JSON | jq -r .releaseVersion)" >> $GITHUB_OUTPUT
+ echo "NPM_TAG=$(echo $VERSION_JSON | jq -r .npmTag)" >> $GITHUB_OUTPUT
+ env:
+ IS_NIGHTLY: ${{ steps.vars.outputs.is_nightly }}
+ MANUAL_VERSION: ${{ inputs.version }}
+
+ - name: Run Tests
+ if: github.event.inputs.force_skip_tests != 'true'
+ run: |
+ npm run preflight
+ npm run test:integration:sandbox:none
+ npm run test:integration:sandbox:docker
+ env:
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+
+ - name: Configure Git User
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Create and switch to a release branch
+ id: release_branch
+ run: |
+ BRANCH_NAME="release/${{ steps.version.outputs.RELEASE_TAG }}"
+ git switch -c $BRANCH_NAME
+ echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_OUTPUT
+
+ - name: Update package versions
+ run: |
+ npm run release:version ${{ steps.version.outputs.RELEASE_VERSION }}
+
+ - name: Commit and Conditionally Push package versions
+ run: |
+ git add package.json package-lock.json packages/*/package.json
+ git commit -m "chore(release): ${{ steps.version.outputs.RELEASE_TAG }}"
+ if [[ "${{ steps.vars.outputs.is_dry_run }}" == "false" ]]; then
+ echo "Pushing release branch to remote..."
+ git push --set-upstream origin ${{ steps.release_branch.outputs.BRANCH_NAME }} --follow-tags
+ else
+ echo "Dry run enabled. Skipping push."
+ fi
+
+ - name: Build and Prepare Packages
+ run: |
+ npm run build:packages
+ npm run prepare:package
+
+ - name: Configure npm for publishing
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '20'
+ registry-url: 'https://wombat-dressing-room.appspot.com'
+ scope: '@google'
+
+ - name: Publish @google/gemini-cli-core
+ run: npm publish --workspace=@google/gemini-cli-core --tag=${{ steps.version.outputs.NPM_TAG }} ${{ steps.vars.outputs.is_dry_run == 'true' && '--dry-run' || '' }}
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.WOMBAT_TOKEN_CORE }}
+
+ - name: Install latest core package
+ if: steps.vars.outputs.is_dry_run == 'false'
+ run: npm install @google/gemini-cli-core@${{ steps.version.outputs.RELEASE_VERSION }} --workspace=@google/gemini-cli --save-exact
+
+ - name: Publish @google/gemini-cli
+ run: npm publish --workspace=@google/gemini-cli --tag=${{ steps.version.outputs.NPM_TAG }} ${{ steps.vars.outputs.is_dry_run == 'true' && '--dry-run' || '' }}
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.WOMBAT_TOKEN_CLI }}
+
+ - name: Create GitHub Release and Tag
+ if: ${{ steps.vars.outputs.is_dry_run == 'false' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ RELEASE_BRANCH: ${{ steps.release_branch.outputs.BRANCH_NAME }}
+ run: |
+ gh release create ${{ steps.version.outputs.RELEASE_TAG }} \
+ bundle/gemini.js \
+ --target "$RELEASE_BRANCH" \
+ --title "Release ${{ steps.version.outputs.RELEASE_TAG }}" \
+ --generate-notes
+
+ - name: Create Issue on Failure
+ if: failure()
+ run: |
+ gh issue create \
+ --title "Release Failed for ${{ steps.version.outputs.RELEASE_TAG || 'N/A' }} on $(date +'%Y-%m-%d')" \
+ --body "The release workflow failed. See the full run for details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
+ --label "kind/bug,release-failure"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..1d2bf251
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,48 @@
+# API keys and secrets
+.env
+.env~
+
+# gemini-cli settings
+.gemini/
+!gemini/config.yaml
+
+# Note: .gemini-clipboard/ is NOT in gitignore so Gemini can access pasted images
+
+# Dependency directory
+node_modules
+bower_components
+
+# Editors
+.idea
+*.iml
+
+# OS metadata
+.DS_Store
+Thumbs.db
+
+# TypeScript build info files
+*.tsbuildinfo
+
+# Ignore built ts files
+dist
+
+# Docker folder to help skip auth refreshes
+.docker
+
+bundle
+
+# Test report files
+junit.xml
+packages/*/coverage/
+
+# Generated files
+packages/cli/src/generated/
+.integration-tests/
+
+
+# Logs
+logs/
+
+
+# Qwen Code Configs
+.qwen/
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 00000000..c48c9dc8
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+@google:registry=https://wombat-dressing-room.appspot.com
+@ali:registry=https://registry.anpm.alibaba-inc.com
\ No newline at end of file
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 00000000..fa9699b8
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "semi": true,
+ "trailingComma": "all",
+ "singleQuote": true,
+ "printWidth": 80,
+ "tabWidth": 2
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..605a464d
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,81 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Launch CLI",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": ["run", "start"],
+ "skipFiles": ["/**"],
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal",
+ "env": {
+ "GEMINI_SANDBOX": "false"
+ }
+ },
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Launch E2E",
+ "program": "${workspaceFolder}/integration-tests/run-tests.js",
+ "args": ["--verbose", "--keep-output", "list_directory"],
+ "skipFiles": ["/**"],
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal",
+ "env": {
+ "GEMINI_SANDBOX": "false"
+ }
+ },
+ {
+ "name": "Attach",
+ "port": 9229,
+ "request": "attach",
+ "skipFiles": ["/**"],
+ "type": "node",
+ // fix source mapping when debugging in sandbox using global installation
+ // note this does not interfere when remoteRoot is also ${workspaceFolder}/packages
+ "remoteRoot": "/usr/local/share/npm-global/lib/node_modules/@gemini-cli",
+ "localRoot": "${workspaceFolder}/packages"
+ },
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Launch Program",
+ "skipFiles": ["/**"],
+ "program": "${file}",
+ "outFiles": ["${workspaceFolder}/**/*.js"]
+ },
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Debug Test File",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": [
+ "run",
+ "test",
+ "-w",
+ "packages",
+ "--",
+ "--inspect-brk=9229",
+ "--no-file-parallelism",
+ "${input:testFile}"
+ ],
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen",
+ "skipFiles": ["/**"]
+ }
+ ],
+ "inputs": [
+ {
+ "id": "testFile",
+ "type": "promptString",
+ "description": "Enter the path to the test file (e.g., ${workspaceFolder}/packages/cli/src/ui/components/LoadingIndicator.test.tsx)",
+ "default": "${workspaceFolder}/packages/cli/src/ui/components/LoadingIndicator.test.tsx"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..1e9031c3
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsserver.experimental.enableProjectDiagnostics": true
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..1ff9a62f
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,16 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "npm",
+ "script": "build",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [],
+ "label": "npm: build",
+ "detail": "scripts/build.sh"
+ }
+ ]
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..dd835a0a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,299 @@
+# How to Contribute
+
+We would love to accept your patches and contributions to this project.
+
+## Before you begin
+
+### Sign our Contributor License Agreement
+
+Contributions to this project must be accompanied by a
+[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
+You (or your employer) retain the copyright to your contribution; this simply
+gives us permission to use and redistribute your contributions as part of the
+project.
+
+If you or your current employer have already signed the Google CLA (even if it
+was for a different project), you probably don't need to do it again.
+
+Visit to see your current agreements or to
+sign a new one.
+
+### Review our Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google/conduct/).
+
+## Contribution Process
+
+### Code Reviews
+
+All submissions, including submissions by project members, require review. We
+use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests)
+for this purpose.
+
+### Pull Request Guidelines
+
+To help us review and merge your PRs quickly, please follow these guidelines. PRs that do not meet these standards may be closed.
+
+#### 1. Link to an Existing Issue
+
+All PRs should be linked to an existing issue in our tracker. This ensures that every change has been discussed and is aligned with the project's goals before any code is written.
+
+- **For bug fixes:** The PR should be linked to the bug report issue.
+- **For features:** The PR should be linked to the feature request or proposal issue that has been approved by a maintainer.
+
+If an issue for your change doesn't exist, please **open one first** and wait for feedback before you start coding.
+
+#### 2. Keep It Small and Focused
+
+We favor small, atomic PRs that address a single issue or add a single, self-contained feature.
+
+- **Do:** Create a PR that fixes one specific bug or adds one specific feature.
+- **Don't:** Bundle multiple unrelated changes (e.g., a bug fix, a new feature, and a refactor) into a single PR.
+
+Large changes should be broken down into a series of smaller, logical PRs that can be reviewed and merged independently.
+
+#### 3. Use Draft PRs for Work in Progress
+
+If you'd like to get early feedback on your work, please use GitHub's **Draft Pull Request** feature. This signals to the maintainers that the PR is not yet ready for a formal review but is open for discussion and initial feedback.
+
+#### 4. Ensure All Checks Pass
+
+Before submitting your PR, ensure that all automated checks are passing by running `npm run preflight`. This command runs all tests, linting, and other style checks.
+
+#### 5. Update Documentation
+
+If your PR introduces a user-facing change (e.g., a new command, a modified flag, or a change in behavior), you must also update the relevant documentation in the `/docs` directory.
+
+#### 6. Write Clear Commit Messages and a Good PR Description
+
+Your PR should have a clear, descriptive title and a detailed description of the changes. Follow the [Conventional Commits](https://www.conventionalcommits.org/) standard for your commit messages.
+
+- **Good PR Title:** `feat(cli): Add --json flag to 'config get' command`
+- **Bad PR Title:** `Made some changes`
+
+In the PR description, explain the "why" behind your changes and link to the relevant issue (e.g., `Fixes #123`).
+
+## Forking
+
+If you are forking the repository you will be able to run the Build, Test and Integration test workflows. However in order to make the integration tests run you'll need to add a [GitHub Repository Secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) with a value of `GEMINI_API_KEY` and set that to a valid API key that you have available. Your key and secret are private to your repo; no one without access can see your key and you cannot see any secrets related to this repo.
+
+Additionally you will need to click on the `Actions` tab and enable workflows for your repository, you'll find it's the large blue button in the center of the screen.
+
+## Development Setup and Workflow
+
+This section guides contributors on how to build, modify, and understand the development setup of this project.
+
+### Setting Up the Development Environment
+
+**Prerequisites:**
+
+1. **Node.js**:
+ - **Development:** Please use Node.js `~20.19.0`. This specific version is required due to an upstream development dependency issue. You can use a tool like [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions.
+ - **Production:** For running the CLI in a production environment, any version of Node.js `>=20` is acceptable.
+2. **Git**
+
+### Build Process
+
+To clone the repository:
+
+```bash
+git clone https://github.com/google-gemini/gemini-cli.git # Or your fork's URL
+cd gemini-cli
+```
+
+To install dependencies defined in `package.json` as well as root dependencies:
+
+```bash
+npm install
+```
+
+To build the entire project (all packages):
+
+```bash
+npm run build
+```
+
+This command typically compiles TypeScript to JavaScript, bundles assets, and prepares the packages for execution. Refer to `scripts/build.js` and `package.json` scripts for more details on what happens during the build.
+
+### Enabling Sandboxing
+
+[Sandboxing](#sandboxing) is highly recommended and requires, at a minimum, setting `GEMINI_SANDBOX=true` in your `~/.env` and ensuring a sandboxing provider (e.g. `macOS Seatbelt`, `docker`, or `podman`) is available. See [Sandboxing](#sandboxing) for details.
+
+To build both the `gemini` CLI utility and the sandbox container, run `build:all` from the root directory:
+
+```bash
+npm run build:all
+```
+
+To skip building the sandbox container, you can use `npm run build` instead.
+
+### Running
+
+To start the Gemini CLI from the source code (after building), run the following command from the root directory:
+
+```bash
+npm start
+```
+
+If you'd like to run the source build outside of the gemini-cli folder you can utilize `npm link path/to/gemini-cli/packages/cli` (see: [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) or `alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`
+
+### Running Tests
+
+This project contains two types of tests: unit tests and integration tests.
+
+#### Unit Tests
+
+To execute the unit test suite for the project:
+
+```bash
+npm run test
+```
+
+This will run tests located in the `packages/core` and `packages/cli` directories. Ensure tests pass before submitting any changes. For a more comprehensive check, it is recommended to run `npm run preflight`.
+
+#### Integration Tests
+
+The integration tests are designed to validate the end-to-end functionality of the Gemini CLI. They are not run as part of the default `npm run test` command.
+
+To run the integration tests, use the following command:
+
+```bash
+npm run test:e2e
+```
+
+For more detailed information on the integration testing framework, please see the [Integration Tests documentation](./docs/integration-tests.md).
+
+### Linting and Preflight Checks
+
+To ensure code quality and formatting consistency, run the preflight check:
+
+```bash
+npm run preflight
+```
+
+This command will run ESLint, Prettier, all tests, and other checks as defined in the project's `package.json`.
+
+_ProTip_
+
+after cloning create a git precommit hook file to ensure your commits are always clean.
+
+```bash
+echo "
+# Run npm build and check for errors
+if ! npm run preflight; then
+ echo "npm build failed. Commit aborted."
+ exit 1
+fi
+" > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
+```
+
+#### Formatting
+
+To separately format the code in this project by running the following command from the root directory:
+
+```bash
+npm run format
+```
+
+This command uses Prettier to format the code according to the project's style guidelines.
+
+#### Linting
+
+To separately lint the code in this project, run the following command from the root directory:
+
+```bash
+npm run lint
+```
+
+### Coding Conventions
+
+- Please adhere to the coding style, patterns, and conventions used throughout the existing codebase.
+- Consult [GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md) (typically found in the project root) for specific instructions related to AI-assisted development, including conventions for React, comments, and Git usage.
+- **Imports:** Pay special attention to import paths. The project uses `eslint-rules/no-relative-cross-package-imports.js` to enforce restrictions on relative imports between packages.
+
+### Project Structure
+
+- `packages/`: Contains the individual sub-packages of the project.
+ - `cli/`: The command-line interface.
+ - `core/`: The core backend logic for the Gemini CLI.
+- `docs/`: Contains all project documentation.
+- `scripts/`: Utility scripts for building, testing, and development tasks.
+
+For more detailed architecture, see `docs/architecture.md`.
+
+## Debugging
+
+### VS Code:
+
+0. Run the CLI to interactively debug in VS Code with `F5`
+1. Start the CLI in debug mode from the root directory:
+ ```bash
+ npm run debug
+ ```
+ This command runs `node --inspect-brk dist/gemini.js` within the `packages/cli` directory, pausing execution until a debugger attaches. You can then open `chrome://inspect` in your Chrome browser to connect to the debugger.
+2. In VS Code, use the "Attach" launch configuration (found in `.vscode/launch.json`).
+
+Alternatively, you can use the "Launch Program" configuration in VS Code if you prefer to launch the currently open file directly, but 'F5' is generally recommended.
+
+To hit a breakpoint inside the sandbox container run:
+
+```bash
+DEBUG=1 gemini
+```
+
+### React DevTools
+
+To debug the CLI's React-based UI, you can use React DevTools. Ink, the library used for the CLI's interface, is compatible with React DevTools version 4.x.
+
+1. **Start the Gemini CLI in development mode:**
+
+ ```bash
+ DEV=true npm start
+ ```
+
+2. **Install and run React DevTools version 4.28.5 (or the latest compatible 4.x version):**
+
+ You can either install it globally:
+
+ ```bash
+ npm install -g react-devtools@4.28.5
+ react-devtools
+ ```
+
+ Or run it directly using npx:
+
+ ```bash
+ npx react-devtools@4.28.5
+ ```
+
+ Your running CLI application should then connect to React DevTools.
+ 
+
+## Sandboxing
+
+### MacOS Seatbelt
+
+On MacOS, `gemini` uses Seatbelt (`sandbox-exec`) under a `permissive-open` profile (see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) that restricts writes to the project folder but otherwise allows all other operations and outbound network traffic ("open") by default. You can switch to a `restrictive-closed` profile (see `packages/cli/src/utils/sandbox-macos-restrictive-closed.sb`) that declines all operations and outbound network traffic ("closed") by default by setting `SEATBELT_PROFILE=restrictive-closed` in your environment or `.env` file. Available built-in profiles are `{permissive,restrictive}-{open,closed,proxied}` (see below for proxied networking). You can also switch to a custom profile `SEATBELT_PROFILE=` if you also create a file `.qwen/sandbox-macos-.sb` under your project settings directory `.gemini`.
+
+### Container-based Sandboxing (All Platforms)
+
+For stronger container-based sandboxing on MacOS or other platforms, you can set `GEMINI_SANDBOX=true|docker|podman|` in your environment or `.env` file. The specified command (or if `true` then either `docker` or `podman`) must be installed on the host machine. Once enabled, `npm run build:all` will build a minimal container ("sandbox") image and `npm start` will launch inside a fresh instance of that container. The first build can take 20-30s (mostly due to downloading of the base image) but after that both build and start overhead should be minimal. Default builds (`npm run build`) will not rebuild the sandbox.
+
+Container-based sandboxing mounts the project directory (and system temp directory) with read-write access and is started/stopped/removed automatically as you start/stop Gemini CLI. Files created within the sandbox should be automatically mapped to your user/group on host machine. You can easily specify additional mounts, ports, or environment variables by setting `SANDBOX_{MOUNTS,PORTS,ENV}` as needed. You can also fully customize the sandbox for your projects by creating the files `.qwen/sandbox.Dockerfile` and/or `.qwen/sandbox.bashrc` under your project settings directory (`.gemini`) and running `gemini` with `BUILD_SANDBOX=1` to trigger building of your custom sandbox.
+
+#### Proxied Networking
+
+All sandboxing methods, including MacOS Seatbelt using `*-proxied` profiles, support restricting outbound network traffic through a custom proxy server that can be specified as `GEMINI_SANDBOX_PROXY_COMMAND=`, where `` must start a proxy server that listens on `:::8877` for relevant requests. See `docs/examples/proxy-script.md` for a minimal proxy that only allows `HTTPS` connections to `example.com:443` (e.g. `curl https://example.com`) and declines all other requests. The proxy is started and stopped automatically alongside the sandbox.
+
+## Manual Publish
+
+We publish an artifact for each commit to our internal registry. But if you need to manually cut a local build, then run the following commands:
+
+```
+npm run clean
+npm install
+npm run auth
+npm run prerelease:dev
+npm publish --workspaces
+```
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..d4f22305
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,50 @@
+FROM docker.io/library/node:20-slim
+
+ARG SANDBOX_NAME="gemini-cli-sandbox"
+ARG CLI_VERSION_ARG
+ENV SANDBOX="$SANDBOX_NAME"
+ENV CLI_VERSION=$CLI_VERSION_ARG
+
+# install minimal set of packages, then clean up
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ python3 \
+ make \
+ g++ \
+ man-db \
+ curl \
+ dnsutils \
+ less \
+ jq \
+ bc \
+ gh \
+ git \
+ unzip \
+ rsync \
+ ripgrep \
+ procps \
+ psmisc \
+ lsof \
+ socat \
+ ca-certificates \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# set up npm global package folder under /usr/local/share
+# give it to non-root user node, already set up in base image
+RUN mkdir -p /usr/local/share/npm-global \
+ && chown -R node:node /usr/local/share/npm-global
+ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
+ENV PATH=$PATH:/usr/local/share/npm-global/bin
+
+# switch to non-root user node
+USER node
+
+# install gemini-cli and clean up
+COPY packages/cli/dist/google-gemini-cli-*.tgz /usr/local/share/npm-global/gemini-cli.tgz
+COPY packages/core/dist/google-gemini-cli-core-*.tgz /usr/local/share/npm-global/gemini-core.tgz
+RUN npm install -g /usr/local/share/npm-global/gemini-cli.tgz /usr/local/share/npm-global/gemini-core.tgz \
+ && npm cache clean --force \
+ && rm -f /usr/local/share/npm-global/gemini-{cli,core}.tgz
+
+# default entrypoint when none specified
+CMD ["gemini"]
diff --git a/LICENSE b/LICENSE
index 261eeb9e..346b3f95 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,4 @@
+
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -186,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright [yyyy] [name of copyright owner]
+ Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..bf8498cf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,59 @@
+# Makefile for gemini-cli
+
+.PHONY: help install build build-sandbox build-all test lint format preflight clean start debug release run-npx create-alias
+
+help:
+ @echo "Makefile for gemini-cli"
+ @echo ""
+ @echo "Usage:"
+ @echo " make install - Install npm dependencies"
+ @echo " make build - Build the main project"
+ @echo " make build-all - Build the main project and sandbox"
+ @echo " make test - Run the test suite"
+ @echo " make lint - Lint the code"
+ @echo " make format - Format the code"
+ @echo " make preflight - Run formatting, linting, and tests"
+ @echo " make clean - Remove generated files"
+ @echo " make start - Start the Gemini CLI"
+ @echo " make debug - Start the Gemini CLI in debug mode"
+ @echo ""
+ @echo " make run-npx - Run the CLI using npx (for testing the published package)"
+ @echo " make create-alias - Create a 'gemini' alias for your shell"
+
+install:
+ npm install
+
+build:
+ npm run build
+
+
+build-all:
+ npm run build:all
+
+test:
+ npm run test
+
+lint:
+ npm run lint
+
+format:
+ npm run format
+
+preflight:
+ npm run preflight
+
+clean:
+ npm run clean
+
+start:
+ npm run start
+
+debug:
+ npm run debug
+
+
+run-npx:
+ npx https://github.com/google-gemini/gemini-cli
+
+create-alias:
+ scripts/create_alias.sh
diff --git a/QWEN.md b/QWEN.md
new file mode 100644
index 00000000..dff8c73e
--- /dev/null
+++ b/QWEN.md
@@ -0,0 +1,184 @@
+## Building and running
+
+Before submitting any changes, it is crucial to validate them by running the full preflight check. This command will build the repository, run all tests, check for type errors, and lint the code.
+
+To run the full suite of checks, execute the following command:
+
+```bash
+npm run preflight
+```
+
+This single command ensures that your changes meet all the quality gates of the project. While you can run the individual steps (`build`, `test`, `typecheck`, `lint`) separately, it is highly recommended to use `npm run preflight` to ensure a comprehensive validation.
+
+## Writing Tests
+
+This project uses **Vitest** as its primary testing framework. When writing tests, aim to follow existing patterns. Key conventions include:
+
+### Test Structure and Framework
+
+- **Framework**: All tests are written using Vitest (`describe`, `it`, `expect`, `vi`).
+- **File Location**: Test files (`*.test.ts` for logic, `*.test.tsx` for React components) are co-located with the source files they test.
+- **Configuration**: Test environments are defined in `vitest.config.ts` files.
+- **Setup/Teardown**: Use `beforeEach` and `afterEach`. Commonly, `vi.resetAllMocks()` is called in `beforeEach` and `vi.restoreAllMocks()` in `afterEach`.
+
+### Mocking (`vi` from Vitest)
+
+- **ES Modules**: Mock with `vi.mock('module-name', async (importOriginal) => { ... })`. Use `importOriginal` for selective mocking.
+ - _Example_: `vi.mock('os', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, homedir: vi.fn() }; });`
+- **Mocking Order**: For critical dependencies (e.g., `os`, `fs`) that affect module-level constants, place `vi.mock` at the _very top_ of the test file, before other imports.
+- **Hoisting**: Use `const myMock = vi.hoisted(() => vi.fn());` if a mock function needs to be defined before its use in a `vi.mock` factory.
+- **Mock Functions**: Create with `vi.fn()`. Define behavior with `mockImplementation()`, `mockResolvedValue()`, or `mockRejectedValue()`.
+- **Spying**: Use `vi.spyOn(object, 'methodName')`. Restore spies with `mockRestore()` in `afterEach`.
+
+### Commonly Mocked Modules
+
+- **Node.js built-ins**: `fs`, `fs/promises`, `os` (especially `os.homedir()`), `path`, `child_process` (`execSync`, `spawn`).
+- **External SDKs**: `@google/genai`, `@modelcontextprotocol/sdk`.
+- **Internal Project Modules**: Dependencies from other project packages are often mocked.
+
+### React Component Testing (CLI UI - Ink)
+
+- Use `render()` from `ink-testing-library`.
+- Assert output with `lastFrame()`.
+- Wrap components in necessary `Context.Provider`s.
+- Mock custom React hooks and complex child components using `vi.mock()`.
+
+### Asynchronous Testing
+
+- Use `async/await`.
+- For timers, use `vi.useFakeTimers()`, `vi.advanceTimersByTimeAsync()`, `vi.runAllTimersAsync()`.
+- Test promise rejections with `await expect(promise).rejects.toThrow(...)`.
+
+### General Guidance
+
+- When adding tests, first examine existing tests to understand and conform to established conventions.
+- Pay close attention to the mocks at the top of existing test files; they reveal critical dependencies and how they are managed in a test environment.
+
+## Git Repo
+
+The main branch for this project is called "main"
+
+## JavaScript/TypeScript
+
+When contributing to this React, Node, and TypeScript codebase, please prioritize the use of plain JavaScript objects with accompanying TypeScript interface or type declarations over JavaScript class syntax. This approach offers significant advantages, especially concerning interoperability with React and overall code maintainability.
+
+### Preferring Plain Objects over Classes
+
+JavaScript classes, by their nature, are designed to encapsulate internal state and behavior. While this can be useful in some object-oriented paradigms, it often introduces unnecessary complexity and friction when working with React's component-based architecture. Here's why plain objects are preferred:
+
+- Seamless React Integration: React components thrive on explicit props and state management. Classes' tendency to store internal state directly within instances can make prop and state propagation harder to reason about and maintain. Plain objects, on the other hand, are inherently immutable (when used thoughtfully) and can be easily passed as props, simplifying data flow and reducing unexpected side effects.
+
+- Reduced Boilerplate and Increased Conciseness: Classes often promote the use of constructors, this binding, getters, setters, and other boilerplate that can unnecessarily bloat code. TypeScript interface and type declarations provide powerful static type checking without the runtime overhead or verbosity of class definitions. This allows for more succinct and readable code, aligning with JavaScript's strengths in functional programming.
+
+- Enhanced Readability and Predictability: Plain objects, especially when their structure is clearly defined by TypeScript interfaces, are often easier to read and understand. Their properties are directly accessible, and there's no hidden internal state or complex inheritance chains to navigate. This predictability leads to fewer bugs and a more maintainable codebase.
+ Simplified Immutability: While not strictly enforced, plain objects encourage an immutable approach to data. When you need to modify an object, you typically create a new one with the desired changes, rather than mutating the original. This pattern aligns perfectly with React's reconciliation process and helps prevent subtle bugs related to shared mutable state.
+
+- Better Serialization and Deserialization: Plain JavaScript objects are naturally easy to serialize to JSON and deserialize back, which is a common requirement in web development (e.g., for API communication or local storage). Classes, with their methods and prototypes, can complicate this process.
+
+### Embracing ES Module Syntax for Encapsulation
+
+Rather than relying on Java-esque private or public class members, which can be verbose and sometimes limit flexibility, we strongly prefer leveraging ES module syntax (`import`/`export`) for encapsulating private and public APIs.
+
+- Clearer Public API Definition: With ES modules, anything that is exported is part of the public API of that module, while anything not exported is inherently private to that module. This provides a very clear and explicit way to define what parts of your code are meant to be consumed by other modules.
+
+- Enhanced Testability (Without Exposing Internals): By default, unexported functions or variables are not accessible from outside the module. This encourages you to test the public API of your modules, rather than their internal implementation details. If you find yourself needing to spy on or stub an unexported function for testing purposes, it's often a "code smell" indicating that the function might be a good candidate for extraction into its own separate, testable module with a well-defined public API. This promotes a more robust and maintainable testing strategy.
+
+- Reduced Coupling: Explicitly defined module boundaries through import/export help reduce coupling between different parts of your codebase. This makes it easier to refactor, debug, and understand individual components in isolation.
+
+### Avoiding `any` Types and Type Assertions; Preferring `unknown`
+
+TypeScript's power lies in its ability to provide static type checking, catching potential errors before your code runs. To fully leverage this, it's crucial to avoid the `any` type and be judicious with type assertions.
+
+- **The Dangers of `any`**: Using any effectively opts out of TypeScript's type checking for that particular variable or expression. While it might seem convenient in the short term, it introduces significant risks:
+ - **Loss of Type Safety**: You lose all the benefits of type checking, making it easy to introduce runtime errors that TypeScript would otherwise have caught.
+ - **Reduced Readability and Maintainability**: Code with `any` types is harder to understand and maintain, as the expected type of data is no longer explicitly defined.
+ - **Masking Underlying Issues**: Often, the need for any indicates a deeper problem in the design of your code or the way you're interacting with external libraries. It's a sign that you might need to refine your types or refactor your code.
+
+- **Preferring `unknown` over `any`**: When you absolutely cannot determine the type of a value at compile time, and you're tempted to reach for any, consider using unknown instead. unknown is a type-safe counterpart to any. While a variable of type unknown can hold any value, you must perform type narrowing (e.g., using typeof or instanceof checks, or a type assertion) before you can perform any operations on it. This forces you to handle the unknown type explicitly, preventing accidental runtime errors.
+
+ ```
+ function processValue(value: unknown) {
+ if (typeof value === 'string') {
+ // value is now safely a string
+ console.log(value.toUpperCase());
+ } else if (typeof value === 'number') {
+ // value is now safely a number
+ console.log(value * 2);
+ }
+ // Without narrowing, you cannot access properties or methods on 'value'
+ // console.log(value.someProperty); // Error: Object is of type 'unknown'.
+ }
+ ```
+
+- **Type Assertions (`as Type`) - Use with Caution**: Type assertions tell the TypeScript compiler, "Trust me, I know what I'm doing; this is definitely of this type." While there are legitimate use cases (e.g., when dealing with external libraries that don't have perfect type definitions, or when you have more information than the compiler), they should be used sparingly and with extreme caution.
+ - **Bypassing Type Checking**: Like `any`, type assertions bypass TypeScript's safety checks. If your assertion is incorrect, you introduce a runtime error that TypeScript would not have warned you about.
+ - **Code Smell in Testing**: A common scenario where `any` or type assertions might be tempting is when trying to test "private" implementation details (e.g., spying on or stubbing an unexported function within a module). This is a strong indication of a "code smell" in your testing strategy and potentially your code structure. Instead of trying to force access to private internals, consider whether those internal details should be refactored into a separate module with a well-defined public API. This makes them inherently testable without compromising encapsulation.
+
+### Embracing JavaScript's Array Operators
+
+To further enhance code cleanliness and promote safe functional programming practices, leverage JavaScript's rich set of array operators as much as possible. Methods like `.map()`, `.filter()`, `.reduce()`, `.slice()`, `.sort()`, and others are incredibly powerful for transforming and manipulating data collections in an immutable and declarative way.
+
+Using these operators:
+
+- Promotes Immutability: Most array operators return new arrays, leaving the original array untouched. This functional approach helps prevent unintended side effects and makes your code more predictable.
+- Improves Readability: Chaining array operators often lead to more concise and expressive code than traditional for loops or imperative logic. The intent of the operation is clear at a glance.
+- Facilitates Functional Programming: These operators are cornerstones of functional programming, encouraging the creation of pure functions that take inputs and produce outputs without causing side effects. This paradigm is highly beneficial for writing robust and testable code that pairs well with React.
+
+By consistently applying these principles, we can maintain a codebase that is not only efficient and performant but also a joy to work with, both now and in the future.
+
+## React (mirrored and adjusted from [react-mcp-server](https://github.com/facebook/react/blob/4448b18760d867f9e009e810571e7a3b8930bb19/compiler/packages/react-mcp-server/src/index.ts#L376C1-L441C94))
+
+### Role
+
+You are a React assistant that helps users write more efficient and optimizable React code. You specialize in identifying patterns that enable React Compiler to automatically apply optimizations, reducing unnecessary re-renders and improving application performance.
+
+### Follow these guidelines in all code you produce and suggest
+
+Use functional components with Hooks: Do not generate class components or use old lifecycle methods. Manage state with useState or useReducer, and side effects with useEffect (or related Hooks). Always prefer functions and Hooks for any new component logic.
+
+Keep components pure and side-effect-free during rendering: Do not produce code that performs side effects (like subscriptions, network requests, or modifying external variables) directly inside the component's function body. Such actions should be wrapped in useEffect or performed in event handlers. Ensure your render logic is a pure function of props and state.
+
+Respect one-way data flow: Pass data down through props and avoid any global mutations. If two components need to share data, lift that state up to a common parent or use React Context, rather than trying to sync local state or use external variables.
+
+Never mutate state directly: Always generate code that updates state immutably. For example, use spread syntax or other methods to create new objects/arrays when updating state. Do not use assignments like state.someValue = ... or array mutations like array.push() on state variables. Use the state setter (setState from useState, etc.) to update state.
+
+Accurately use useEffect and other effect Hooks: whenever you think you could useEffect, think and reason harder to avoid it. useEffect is primarily only used for synchronization, for example synchronizing React with some external state. IMPORTANT - Don't setState (the 2nd value returned by useState) within a useEffect as that will degrade performance. When writing effects, include all necessary dependencies in the dependency array. Do not suppress ESLint rules or omit dependencies that the effect's code uses. Structure the effect callbacks to handle changing values properly (e.g., update subscriptions on prop changes, clean up on unmount or dependency change). If a piece of logic should only run in response to a user action (like a form submission or button click), put that logic in an event handler, not in a useEffect. Where possible, useEffects should return a cleanup function.
+
+Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect, useContext, custom Hooks, etc.) are called unconditionally at the top level of React function components or other Hooks. Do not generate code that calls Hooks inside loops, conditional statements, or nested helper functions. Do not call Hooks in non-component functions or outside the React component rendering context.
+
+Use refs only when necessary: Avoid using useRef unless the task genuinely requires it (such as focusing a control, managing an animation, or integrating with a non-React library). Do not use refs to store application state that should be reactive. If you do use refs, never write to or read from ref.current during the rendering of a component (except for initial setup like lazy initialization). Any ref usage should not affect the rendered output directly.
+
+Prefer composition and small components: Break down UI into small, reusable components rather than writing large monolithic components. The code you generate should promote clarity and reusability by composing components together. Similarly, abstract repetitive logic into custom Hooks when appropriate to avoid duplicating code.
+
+Optimize for concurrency: Assume React may render your components multiple times for scheduling purposes (especially in development with Strict Mode). Write code that remains correct even if the component function runs more than once. For instance, avoid side effects in the component body and use functional state updates (e.g., setCount(c => c + 1)) when updating state based on previous state to prevent race conditions. Always include cleanup functions in effects that subscribe to external resources. Don't write useEffects for "do this when this changes" side effects. This ensures your generated code will work with React's concurrent rendering features without issues.
+
+Optimize to reduce network waterfalls - Use parallel data fetching wherever possible (e.g., start multiple requests at once rather than one after another). Leverage Suspense for data loading and keep requests co-located with the component that needs the data. In a server-centric approach, fetch related data together in a single request on the server side (using Server Components, for example) to reduce round trips. Also, consider using caching layers or global fetch management to avoid repeating identical requests.
+
+Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if React Compiler is enabled. Avoid premature optimization with manual memoization. Instead, focus on writing clear, simple components with direct data flow and side-effect-free render functions. Let the React Compiler handle tree-shaking, inlining, and other performance enhancements to keep your code base simpler and more maintainable.
+
+Design for a good user experience - Provide clear, minimal, and non-blocking UI states. When data is loading, show lightweight placeholders (e.g., skeleton screens) rather than intrusive spinners everywhere. Handle errors gracefully with a dedicated error boundary or a friendly inline message. Where possible, render partial data as it becomes available rather than making the user wait for everything. Suspense allows you to declare the loading states in your component tree in a natural way, preventing โflashโ states and improving perceived performance.
+
+### Process
+
+1. Analyze the user's code for optimization opportunities:
+ - Check for React anti-patterns that prevent compiler optimization
+ - Look for component structure issues that limit compiler effectiveness
+ - Think about each suggestion you are making and consult React docs for best practices
+
+2. Provide actionable guidance:
+ - Explain specific code changes with clear reasoning
+ - Show before/after examples when suggesting changes
+ - Only suggest changes that meaningfully improve optimization potential
+
+### Optimization Guidelines
+
+- State updates should be structured to enable granular updates
+- Side effects should be isolated and dependencies clearly defined
+
+## Comments policy
+
+Only write high-value comments if at all. Avoid talking to the user through comments.
+
+## General style requirements
+
+Use hyphens instead of underscores in flag names (e.g. `my-flag` instead of `my_flag`).
diff --git a/README.gemini.md b/README.gemini.md
new file mode 100644
index 00000000..93fc543e
--- /dev/null
+++ b/README.gemini.md
@@ -0,0 +1,162 @@
+# Gemini CLI
+
+[](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml)
+
+
+
+This repository contains the Gemini CLI, a command-line AI workflow tool that connects to your
+tools, understands your code and accelerates your workflows.
+
+With the Gemini CLI you can:
+
+- Query and edit large codebases in and beyond Gemini's 1M token context window.
+- Generate new apps from PDFs or sketches, using Gemini's multimodal capabilities.
+- Automate operational tasks, like querying pull requests or handling complex rebases.
+- Use tools and MCP servers to connect new capabilities, including [media generation with Imagen,
+ Veo or Lyria](https://github.com/GoogleCloudPlatform/vertex-ai-creative-studio/tree/main/experiments/mcp-genmedia)
+- Ground your queries with the [Google Search](https://ai.google.dev/gemini-api/docs/grounding)
+ tool, built in to Gemini.
+
+## Quickstart
+
+1. **Prerequisites:** Ensure you have [Node.js version 20](https://nodejs.org/en/download) or higher installed.
+2. **Run the CLI:** Execute the following command in your terminal:
+
+ ```bash
+ npx https://github.com/google-gemini/gemini-cli
+ ```
+
+ Or install it with:
+
+ ```bash
+ npm install -g @google/gemini-cli
+ ```
+
+ Then, run the CLI from anywhere:
+
+ ```bash
+ gemini
+ ```
+
+3. **Pick a color theme**
+4. **Authenticate:** When prompted, sign in with your personal Google account. This will grant you up to 60 model requests per minute and 1,000 model requests per day using Gemini.
+
+You are now ready to use the Gemini CLI!
+
+### Use a Gemini API key:
+
+The Gemini API provides a free tier with [100 requests per day](https://ai.google.dev/gemini-api/docs/rate-limits#free-tier) using Gemini 2.5 Pro, control over which model you use, and access to higher rate limits (with a paid plan):
+
+1. Generate a key from [Google AI Studio](https://aistudio.google.com/apikey).
+2. Set it as an environment variable in your terminal. Replace `YOUR_API_KEY` with your generated key.
+
+ ```bash
+ export GEMINI_API_KEY="YOUR_API_KEY"
+ ```
+
+3. (Optionally) Upgrade your Gemini API project to a paid plan on the API key page (will automatically unlock [Tier 1 rate limits](https://ai.google.dev/gemini-api/docs/rate-limits#tier-1))
+
+### Use a Vertex AI API key:
+
+The Vertex AI API provides a [free tier](https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview) using express mode for Gemini 2.5 Pro, control over which model you use, and access to higher rate limits with a billing account:
+
+1. Generate a key from [Google Cloud](https://cloud.google.com/vertex-ai/generative-ai/docs/start/api-keys).
+2. Set it as an environment variable in your terminal. Replace `YOUR_API_KEY` with your generated key and set GOOGLE_GENAI_USE_VERTEXAI to true
+
+ ```bash
+ export GOOGLE_API_KEY="YOUR_API_KEY"
+ export GOOGLE_GENAI_USE_VERTEXAI=true
+ ```
+
+3. (Optionally) Add a billing account on your project to get access to [higher usage limits](https://cloud.google.com/vertex-ai/generative-ai/docs/quotas)
+
+For other authentication methods, including Google Workspace accounts, see the [authentication](./docs/cli/authentication.md) guide.
+
+## Examples
+
+Once the CLI is running, you can start interacting with Gemini from your shell.
+
+You can start a project from a new directory:
+
+```sh
+cd new-project/
+gemini
+> Write me a Gemini Discord bot that answers questions using a FAQ.md file I will provide
+```
+
+Or work with an existing project:
+
+```sh
+git clone https://github.com/google-gemini/gemini-cli
+cd gemini-cli
+gemini
+> Give me a summary of all of the changes that went in yesterday
+```
+
+### Next steps
+
+- Learn how to [contribute to or build from the source](./CONTRIBUTING.md).
+- Explore the available **[CLI Commands](./docs/cli/commands.md)**.
+- If you encounter any issues, review the **[troubleshooting guide](./docs/troubleshooting.md)**.
+- For more comprehensive documentation, see the [full documentation](./docs/index.md).
+- Take a look at some [popular tasks](#popular-tasks) for more inspiration.
+- Check out our **[Official Roadmap](./ROADMAP.md)**
+
+### Troubleshooting
+
+Head over to the [troubleshooting guide](docs/troubleshooting.md) if you're
+having issues.
+
+## Popular tasks
+
+### Explore a new codebase
+
+Start by `cd`ing into an existing or newly-cloned repository and running `gemini`.
+
+```text
+> Describe the main pieces of this system's architecture.
+```
+
+```text
+> What security mechanisms are in place?
+```
+
+### Work with your existing code
+
+```text
+> Implement a first draft for GitHub issue #123.
+```
+
+```text
+> Help me migrate this codebase to the latest version of Java. Start with a plan.
+```
+
+### Automate your workflows
+
+Use MCP servers to integrate your local system tools with your enterprise collaboration suite.
+
+```text
+> Make me a slide deck showing the git history from the last 7 days, grouped by feature and team member.
+```
+
+```text
+> Make a full-screen web app for a wall display to show our most interacted-with GitHub issues.
+```
+
+### Interact with your system
+
+```text
+> Convert all the images in this directory to png, and rename them to use dates from the exif data.
+```
+
+```text
+> Organize my PDF invoices by month of expenditure.
+```
+
+### Uninstall
+
+Head over to the [Uninstall](docs/Uninstall.md) guide for uninstallation instructions.
+
+## Terms of Service and Privacy Notice
+
+For details on the terms of service and privacy notice applicable to your use of Gemini CLI, see the [Terms of Service and Privacy Notice](./docs/tos-privacy.md).
diff --git a/README.md b/README.md
index 26f87b36..94fb99b2 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,131 @@
# Qwen Code
-Qwen Code is a coding agent that lives in digital world
+
+
+
+Qwen Code is a command-line AI workflow tool adapted from [**Gemini CLI**](https://github.com/google-gemini/gemini-cli)(Please refer to [this document](./README.gemini.md) for more details), optimized for Qwen-Coder models with enhanced parser support & tool support.
+
+## Key Features
+
+- **Code Understanding & Editing** - Query and edit large codebases beyond traditional context window limits
+- **Workflow Automation** - Automate operational tasks like handling pull requests and complex rebases
+- **Enhanced Parser** - Adapted parser specifically optimized for Qwen-Coder models
+
+## Quick Start
+
+### Prerequisites
+
+Ensure you have [Node.js version 20](https://nodejs.org/en/download) or higher installed.
+
+```bash
+curl -qL https://www.npmjs.com/install.sh | sh
+```
+
+### Installation
+
+```bash
+npm install -g @qwen-code/qwen-code
+qwen --version
+```
+
+Then run from anywhere:
+
+```bash
+qwen
+```
+
+Or you can install it from source:
+
+```bash
+git clone https://github.com/qwen-code/qwen-code.git
+cd qwen-code
+npm install
+npm install -g .
+```
+
+### API Configuration
+
+Set your Qwen API key (In Qwen Code project, you can also set your API key in `.env` file):
+
+```bash
+export OPENAI_API_KEY="your_api_key_here"
+export OPENAI_BASE_URL="your_api_base_url_here"
+export OPENAI_MODEL="your_api_model_here"
+```
+
+## Usage Examples
+
+### Explore Codebases
+
+```sh
+cd your-project/
+qwen
+> Describe the main pieces of this system's architecture
+```
+
+### Code Development
+
+```sh
+> Refactor this function to improve readability and performance
+```
+
+### Automate Workflows
+
+```sh
+> Analyze git commits from the last 7 days, grouped by feature and team member
+```
+
+```sh
+> Convert all images in this directory to PNG format
+```
+
+## Popular Tasks
+
+### Understand New Codebases
+
+```text
+> What are the core business logic components?
+> What security mechanisms are in place?
+> How does the data flow work?
+```
+
+### Code Refactoring & Optimization
+
+```text
+> What parts of this module can be optimized?
+> Help me refactor this class to follow better design patterns
+> Add proper error handling and logging
+```
+
+### Documentation & Testing
+
+```text
+> Generate comprehensive JSDoc comments for this function
+> Write unit tests for this component
+> Create API documentation
+```
+
+## Project Structure
+
+```
+qwen-code/
+โโโ packages/ # Core packages
+โโโ docs/ # Documentation
+โโโ examples/ # Example code
+โโโ tests/ # Test files
+```
+
+## Development & Contributing
+
+See [CONTRIBUTING.md](./CONTRIBUTING.md) to learn how to contribute to the project.
+
+## Troubleshooting
+
+If you encounter issues, check the [troubleshooting guide](docs/troubleshooting.md).
+
+## Acknowledgments
+
+This project is based on [Google Gemini CLI](https://github.com/google-gemini/gemini-cli). We acknowledge and appreciate the excellent work of the Gemini CLI team. Our main contribution focuses on parser-level adaptations to better support Qwen-Coder models.
+
+## License
+
+[LICENSE](./LICENSE)
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 00000000..9c47a4dd
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,63 @@
+# Gemini CLI Roadmap
+
+The [Official Gemini CLI Roadmap](https://github.com/orgs/google-gemini/projects/11/)
+
+Gemini CLI is an open-source AI agent that brings the power of Gemini directly into your terminal. It provides lightweight access to Gemini, giving you the most direct path from your prompt to our model.
+
+This document outlines our approach to the Gemini CLI roadmap. Here, you'll find our guiding principles and a breakdown of the key areas we are
+focused on for development. Our roadmap is not a static list but a dynamic set of priorities that are tracked live in our GitHub Issues.
+
+As an [Apache 2.0 open source project](https://github.com/google-gemini/gemini-cli?tab=Apache-2.0-1-ov-file#readme), we appreciate and welcome [public contributions](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md), and will give first priority to those contributions aligned with our roadmap. If you want to propose a new feature or change to our roadmap, please start by [opening an issue for discussion](https://github.com/google-gemini/gemini-cli/issues/new/choose).
+
+## Disclaimer
+
+This roadmap represents our current thinking and is for informational purposes only. It is not a commitment or a guarantee of future delivery. The development, release, and timing of any features are subject to change, and we may update the roadmap based on community discussions as well as when our priorities evolve.
+
+## Guiding Principles
+
+Our development is guided by the following principles:
+
+- **Power & Simplicity:** Deliver access to state-of-the-art Gemini models with an intuitive and easy-to-use lightweight command-line interface.
+- **Extensibility:** An adaptable agent to help you with a variety of use cases and environments along with the ability to run these agents anywhere.
+- **Intelligent:** Gemini CLI should be reliably ranked among the best agentic tools as measured by benchmarks like SWE Bench, Terminal Bench, and CSAT.
+- **Free and Open Source:** Foster a thriving open source community where cost isnโt a barrier to personal use, and PRs get merged quickly. This means resolving and closing issues, pull requests, and discussion posts quickly.
+
+## How the Roadmap Works
+
+Our roadmap is managed directly through Github Issues. See our entry point Roadmap Issue [here](https://github.com/google-gemini/gemini-cli/issues/4191). This approach allows for transparency and gives you a direct way to learn more or get involved with any specific initiative. All our roadmap items will be tagged as Type:`Feature` and Label:`maintainer` for features we are actively working on, or Type:`Task` and Label:`maintainer` for a more detailed list of tasks.
+
+Issues are organized to provide key information at a glance:
+
+- **Target Quarter:** `Milestone` denotes the anticipated delivery timeline.
+- **Feature Area:** Labels such as `area/model` or `area/tooling` categorizes the work.
+- **Issue Type:** _Workstream_ => _Epics_ => _Features_ => _Tasks|Bugs_
+
+To see what we're working on, you can filter our issues by these dimensions. See all our items [here](https://github.com/orgs/google-gemini/projects/11/views/19)
+
+## Focus Areas
+
+To better organize our efforts, we categorize our work into several key feature areas. These labels are used on our GitHub Issues to help you filter and
+find initiatives that interest you.
+
+- **Authentication:** Secure user access via API keys, Gemini Code Assist login etc.
+- **Model:** Support new Gemini models, multi-modality, local execution, and performance tuning.
+- **User Experience:** Improve the CLI's usability, performance, interactive features, and documentation.
+- **Tooling:** Built-in tools and the MCP ecosystem.
+- **Core:** Core functionality of the CLI
+- **Extensibility:** Bringing Gemini CLI to other surfaces e.g. GitHub.
+- **Contribution:** Improve the contribution process via test automation and CI/CD pipeline enhancements.
+- **Platform:** Manage installation, OS support, and the underlying CLI framework.
+- **Quality:** Focus on testing, reliability, performance, and overall product quality.
+- **Background Agents:** Enable long-running, autonomous tasks and proactive assistance.
+- **Security and Privacy:** For all things related to security and privacy
+
+## How to Contribute
+
+Gemini CLI is an open-source project, and we welcome contributions from the community! Whether you're a developer, a designer, or just an enthusiastic user you can find our [Community Guidelines here](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md) to learn how to get started. There are many ways to get involved:
+
+- **Roadmap:** Please review and find areas in our [roadmap](https://github.com/google-gemini/gemini-cli/issues/4191) that you would like to contribute to. Contributions based on this will be easiest to integrate with.
+- **Report Bugs:** If you find an issue, please create a bug(https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml) with as much detail as possible. If you believe it is a critical breaking issue preventing direct CLI usage, please tag it as `priorty/p0`.
+- **Suggest Features:** Have a great idea? We'd love to hear it! Open a [feature request](https://github.com/google-gemini/gemini-cli/issues/new?template=feature_request.yml).
+- **Contribute Code:** Check out our [CONTRIBUTING.md](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md) file for guidelines on how to submit pull requests. We have a list of "good first issues" for new contributors.
+- **Write Documentation:** Help us improve our documentation, tutorials, and examples.
+ We are excited about the future of Gemini CLI and look forward to building it with you!
diff --git a/docs/Uninstall.md b/docs/Uninstall.md
new file mode 100644
index 00000000..1aa60336
--- /dev/null
+++ b/docs/Uninstall.md
@@ -0,0 +1,42 @@
+# Uninstalling the CLI
+
+Your uninstall method depends on how you ran the CLI. Follow the instructions for either npx or a global npm installation.
+
+## Method 1: Using npx
+
+npx runs packages from a temporary cache without a permanent installation. To "uninstall" the CLI, you must clear this cache, which will remove gemini-cli and any other packages previously executed with npx.
+
+The npx cache is a directory named `_npx` inside your main npm cache folder. You can find your npm cache path by running `npm config get cache`.
+
+**For macOS / Linux**
+
+```bash
+# The path is typically ~/.npm/_npx
+rm -rf "$(npm config get cache)/_npx"
+```
+
+**For Windows**
+
+_Command Prompt_
+
+```cmd
+:: The path is typically %LocalAppData%\npm-cache\_npx
+rmdir /s /q "%LocalAppData%\npm-cache\_npx"
+```
+
+_PowerShell_
+
+```powershell
+# The path is typically $env:LocalAppData\npm-cache\_npx
+Remove-Item -Path (Join-Path $env:LocalAppData "npm-cache\_npx") -Recurse -Force
+```
+
+## Method 2: Using npm (Global Install)
+
+If you installed the CLI globally (e.g., `npm install -g @google/gemini-cli`), use the `npm uninstall` command with the `-g` flag to remove it.
+
+```bash
+npm uninstall -g @google/gemini-cli
+```
+
+This command completely removes the package from your system.
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 00000000..b120cb25
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,54 @@
+# Gemini CLI Architecture Overview
+
+This document provides a high-level overview of the Gemini CLI's architecture.
+
+## Core components
+
+The Gemini CLI is primarily composed of two main packages, along with a suite of tools that can be used by the system in the course of handling command-line input:
+
+1. **CLI package (`packages/cli`):**
+ - **Purpose:** This contains the user-facing portion of the Gemini CLI, such as handling the initial user input, presenting the final output, and managing the overall user experience.
+ - **Key functions contained in the package:**
+ - [Input processing](./cli/commands.md)
+ - History management
+ - Display rendering
+ - [Theme and UI customization](./cli/themes.md)
+ - [CLI configuration settings](./cli/configuration.md)
+
+2. **Core package (`packages/core`):**
+ - **Purpose:** This acts as the backend for the Gemini CLI. It receives requests sent from `packages/cli`, orchestrates interactions with the Gemini API, and manages the execution of available tools.
+ - **Key functions contained in the package:**
+ - API client for communicating with the Google Gemini API
+ - Prompt construction and management
+ - Tool registration and execution logic
+ - State management for conversations or sessions
+ - Server-side configuration
+
+3. **Tools (`packages/core/src/tools/`):**
+ - **Purpose:** These are individual modules that extend the capabilities of the Gemini model, allowing it to interact with the local environment (e.g., file system, shell commands, web fetching).
+ - **Interaction:** `packages/core` invokes these tools based on requests from the Gemini model.
+
+## Interaction Flow
+
+A typical interaction with the Gemini CLI follows this flow:
+
+1. **User input:** The user types a prompt or command into the terminal, which is managed by `packages/cli`.
+2. **Request to core:** `packages/cli` sends the user's input to `packages/core`.
+3. **Request processed:** The core package:
+ - Constructs an appropriate prompt for the Gemini API, possibly including conversation history and available tool definitions.
+ - Sends the prompt to the Gemini API.
+4. **Gemini API response:** The Gemini API processes the prompt and returns a response. This response might be a direct answer or a request to use one of the available tools.
+5. **Tool execution (if applicable):**
+ - When the Gemini API requests a tool, the core package prepares to execute it.
+ - If the requested tool can modify the file system or execute shell commands, the user is first given details of the tool and its arguments, and the user must approve the execution.
+ - Read-only operations, such as reading files, might not require explicit user confirmation to proceed.
+ - Once confirmed, or if confirmation is not required, the core package executes the relevant action within the relevant tool, and the result is sent back to the Gemini API by the core package.
+ - The Gemini API processes the tool result and generates a final response.
+6. **Response to CLI:** The core package sends the final response back to the CLI package.
+7. **Display to user:** The CLI package formats and displays the response to the user in the terminal.
+
+## Key Design Principles
+
+- **Modularity:** Separating the CLI (frontend) from the Core (backend) allows for independent development and potential future extensions (e.g., different frontends for the same backend).
+- **Extensibility:** The tool system is designed to be extensible, allowing new capabilities to be added.
+- **User experience:** The CLI focuses on providing a rich and interactive terminal experience.
diff --git a/docs/assets/connected_devtools.png b/docs/assets/connected_devtools.png
new file mode 100644
index 00000000..34a3c568
Binary files /dev/null and b/docs/assets/connected_devtools.png differ
diff --git a/docs/assets/gemini-screenshot.png b/docs/assets/gemini-screenshot.png
new file mode 100644
index 00000000..1cc163d8
Binary files /dev/null and b/docs/assets/gemini-screenshot.png differ
diff --git a/docs/assets/qwen-screenshot.png b/docs/assets/qwen-screenshot.png
new file mode 100644
index 00000000..01e60d5f
Binary files /dev/null and b/docs/assets/qwen-screenshot.png differ
diff --git a/docs/assets/theme-ansi-light.png b/docs/assets/theme-ansi-light.png
new file mode 100644
index 00000000..9766ae78
Binary files /dev/null and b/docs/assets/theme-ansi-light.png differ
diff --git a/docs/assets/theme-ansi.png b/docs/assets/theme-ansi.png
new file mode 100644
index 00000000..5d46daca
Binary files /dev/null and b/docs/assets/theme-ansi.png differ
diff --git a/docs/assets/theme-atom-one.png b/docs/assets/theme-atom-one.png
new file mode 100644
index 00000000..c2787d6b
Binary files /dev/null and b/docs/assets/theme-atom-one.png differ
diff --git a/docs/assets/theme-ayu-light.png b/docs/assets/theme-ayu-light.png
new file mode 100644
index 00000000..f1774656
Binary files /dev/null and b/docs/assets/theme-ayu-light.png differ
diff --git a/docs/assets/theme-ayu.png b/docs/assets/theme-ayu.png
new file mode 100644
index 00000000..99391f82
Binary files /dev/null and b/docs/assets/theme-ayu.png differ
diff --git a/docs/assets/theme-default-light.png b/docs/assets/theme-default-light.png
new file mode 100644
index 00000000..829d4ed5
Binary files /dev/null and b/docs/assets/theme-default-light.png differ
diff --git a/docs/assets/theme-default.png b/docs/assets/theme-default.png
new file mode 100644
index 00000000..0b93a334
Binary files /dev/null and b/docs/assets/theme-default.png differ
diff --git a/docs/assets/theme-dracula.png b/docs/assets/theme-dracula.png
new file mode 100644
index 00000000..27213fbc
Binary files /dev/null and b/docs/assets/theme-dracula.png differ
diff --git a/docs/assets/theme-github-light.png b/docs/assets/theme-github-light.png
new file mode 100644
index 00000000..3cdc94aa
Binary files /dev/null and b/docs/assets/theme-github-light.png differ
diff --git a/docs/assets/theme-github.png b/docs/assets/theme-github.png
new file mode 100644
index 00000000..a62961b6
Binary files /dev/null and b/docs/assets/theme-github.png differ
diff --git a/docs/assets/theme-google-light.png b/docs/assets/theme-google-light.png
new file mode 100644
index 00000000..835ebc4b
Binary files /dev/null and b/docs/assets/theme-google-light.png differ
diff --git a/docs/assets/theme-xcode-light.png b/docs/assets/theme-xcode-light.png
new file mode 100644
index 00000000..eb056a55
Binary files /dev/null and b/docs/assets/theme-xcode-light.png differ
diff --git a/docs/checkpointing.md b/docs/checkpointing.md
new file mode 100644
index 00000000..a678f783
--- /dev/null
+++ b/docs/checkpointing.md
@@ -0,0 +1,75 @@
+# Checkpointing
+
+The Gemini CLI includes a Checkpointing feature that automatically saves a snapshot of your project's state before any file modifications are made by AI-powered tools. This allows you to safely experiment with and apply code changes, knowing you can instantly revert back to the state before the tool was run.
+
+## How It Works
+
+When you approve a tool that modifies the file system (like `write_file` or `replace`), the CLI automatically creates a "checkpoint." This checkpoint includes:
+
+1. **A Git Snapshot:** A commit is made in a special, shadow Git repository located in your home directory (`~/.qwen/history/`). This snapshot captures the complete state of your project files at that moment. It does **not** interfere with your own project's Git repository.
+2. **Conversation History:** The entire conversation you've had with the agent up to that point is saved.
+3. **The Tool Call:** The specific tool call that was about to be executed is also stored.
+
+If you want to undo the change or simply go back, you can use the `/restore` command. Restoring a checkpoint will:
+
+- Revert all files in your project to the state captured in the snapshot.
+- Restore the conversation history in the CLI.
+- Re-propose the original tool call, allowing you to run it again, modify it, or simply ignore it.
+
+All checkpoint data, including the Git snapshot and conversation history, is stored locally on your machine. The Git snapshot is stored in the shadow repository while the conversation history and tool calls are saved in a JSON file in your project's temporary directory, typically located at `~/.qwen/tmp//checkpoints`.
+
+## Enabling the Feature
+
+The Checkpointing feature is disabled by default. To enable it, you can either use a command-line flag or edit your `settings.json` file.
+
+### Using the Command-Line Flag
+
+You can enable checkpointing for the current session by using the `--checkpointing` flag when starting the Gemini CLI:
+
+```bash
+gemini --checkpointing
+```
+
+### Using the `settings.json` File
+
+To enable checkpointing by default for all sessions, you need to edit your `settings.json` file.
+
+Add the following key to your `settings.json`:
+
+```json
+{
+ "checkpointing": {
+ "enabled": true
+ }
+}
+```
+
+## Using the `/restore` Command
+
+Once enabled, checkpoints are created automatically. To manage them, you use the `/restore` command.
+
+### List Available Checkpoints
+
+To see a list of all saved checkpoints for the current project, simply run:
+
+```
+/restore
+```
+
+The CLI will display a list of available checkpoint files. These file names are typically composed of a timestamp, the name of the file being modified, and the name of the tool that was about to be run (e.g., `2025-06-22T10-00-00_000Z-my-file.txt-write_file`).
+
+### Restore a Specific Checkpoint
+
+To restore your project to a specific checkpoint, use the checkpoint file from the list:
+
+```
+/restore
+```
+
+For example:
+
+```
+/restore 2025-06-22T10-00-00_000Z-my-file.txt-write_file
+```
+
+After running the command, your files and conversation will be immediately restored to the state they were in when the checkpoint was created, and the original tool prompt will reappear.
diff --git a/docs/cli/authentication.md b/docs/cli/authentication.md
new file mode 100644
index 00000000..81d9a175
--- /dev/null
+++ b/docs/cli/authentication.md
@@ -0,0 +1,114 @@
+# Authentication Setup
+
+The Qwen Code CLI supports multiple authentication methods. On initial startup you'll need to configure **one** of the following authentication methods:
+
+1. **Login with Google (Gemini Code Assist):**
+ - Use this option to log in with your google account.
+ - During initial startup, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs.
+ - Note that the web login must be done in a browser that can communicate with the machine Gemini CLI is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on).
+ - Users may have to specify a GOOGLE_CLOUD_PROJECT if:
+ 1. You have a Google Workspace account. Google Workspace is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school.
+ 1. You have received a free Code Assist license through the [Google Developer Program](https://developers.google.com/program/plans-and-pricing) (including qualified Google Developer Experts)
+ 1. You have been assigned a license to a current Gemini Code Assist standard or enterprise subscription.
+ 1. You are using the product outside the [supported regions](https://developers.google.com/gemini-code-assist/resources/available-locations) for free individual usage.
+ 1. You are a Google account holder under the age of 18
+ - If you fall into one of these categories, you must first configure a Google Cloud Project Id to use, [enable the Gemini for Cloud API](https://cloud.google.com/gemini/docs/discover/set-up-gemini#enable-api) and [configure access permissions](https://cloud.google.com/gemini/docs/discover/set-up-gemini#grant-iam).
+
+ You can temporarily set the environment variable in your current shell session using the following command:
+
+ ```bash
+ export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
+ ```
+ - For repeated use, you can add the environment variable to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
+
+ ```bash
+ echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
+ source ~/.bashrc
+ ```
+
+2. **Gemini API key:**
+ - Obtain your API key from Google AI Studio: [https://aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
+ - Set the `GEMINI_API_KEY` environment variable. In the following methods, replace `YOUR_GEMINI_API_KEY` with the API key you obtained from Google AI Studio:
+ - You can temporarily set the environment variable in your current shell session using the following command:
+ ```bash
+ export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
+ ```
+ - For repeated use, you can add the environment variable to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
+ ```bash
+ echo 'export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"' >> ~/.bashrc
+ source ~/.bashrc
+ ```
+
+3. **Vertex AI:**
+ - Obtain your Google Cloud API key: [Get an API Key](https://cloud.google.com/vertex-ai/generative-ai/docs/start/api-keys?usertype=newuser)
+ - Set the `GOOGLE_API_KEY` environment variable. In the following methods, replace `YOUR_GOOGLE_API_KEY` with your Vertex AI API key:
+ - You can temporarily set these environment variables in your current shell session using the following commands:
+ ```bash
+ export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
+ ```
+ - For repeated use, you can add the environment variables to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands add the environment variables to a `~/.bashrc` file:
+ ```bash
+ echo 'export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"' >> ~/.bashrc
+ source ~/.bashrc
+ ```
+ - To use Application Default Credentials (ADC), use the following command:
+ - Ensure you have a Google Cloud project and have enabled the Vertex AI API.
+ ```bash
+ gcloud auth application-default login
+ ```
+ For more information, see [Set up Application Default Credentials for Google Cloud](https://cloud.google.com/docs/authentication/provide-credentials-adc).
+ - Set the `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` environment variables. In the following methods, replace `YOUR_PROJECT_ID` and `YOUR_PROJECT_LOCATION` with the relevant values for your project:
+ - You can temporarily set these environment variables in your current shell session using the following commands:
+ ```bash
+ export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
+ export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION" # e.g., us-central1
+ ```
+ - For repeated use, you can add the environment variables to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands add the environment variables to a `~/.bashrc` file:
+ ```bash
+ echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
+ echo 'export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"' >> ~/.bashrc
+ source ~/.bashrc
+ ```
+4. **Cloud Shell:**
+ - This option is only available when running in a Google Cloud Shell environment.
+ - It automatically uses the credentials of the logged-in user in the Cloud Shell environment.
+ - This is the default authentication method when running in Cloud Shell and no other method is configured.
+
+### Persisting Environment Variables with `.env` Files
+
+You can create a **`.qwen/.env`** file in your project directory or in your home directory. Creating a plain **`.env`** file also works, but `.qwen/.env` is recommended to keep Gemini variables isolated from other tools.
+
+Gemini CLI automatically loads environment variables from the **first** `.env` file it finds, using the following search order:
+
+1. Starting in the **current directory** and moving upward toward `/`, for each directory it checks:
+ 1. `.qwen/.env`
+ 2. `.env`
+2. If no file is found, it falls back to your **home directory**:
+ - `~/.qwen/.env`
+ - `~/.env`
+
+> **Important:** The search stops at the **first** file encounteredโvariables are **not merged** across multiple files.
+
+#### Examples
+
+**Project-specific overrides** (take precedence when you are inside the project):
+
+```bash
+mkdir -p .gemini
+echo 'GOOGLE_CLOUD_PROJECT="your-project-id"' >> .qwen/.env
+```
+
+**User-wide settings** (available in every directory):
+
+```bash
+mkdir -p ~/.gemini
+cat >> ~/.qwen/.env <<'EOF'
+GOOGLE_CLOUD_PROJECT="your-project-id"
+GEMINI_API_KEY="your-gemini-api-key"
+EOF
+```
+
+5. **OpenAI Authentication:**
+ - Use OpenAI models instead of Google's Gemini models
+ - For detailed setup instructions, see [OpenAI Authentication](./openai-auth.md)
+ - Supports interactive setup, command line arguments, and environment variables
diff --git a/docs/cli/commands.md b/docs/cli/commands.md
new file mode 100644
index 00000000..bcdbc47c
--- /dev/null
+++ b/docs/cli/commands.md
@@ -0,0 +1,138 @@
+# CLI Commands
+
+Gemini CLI supports several built-in commands to help you manage your session, customize the interface, and control its behavior. These commands are prefixed with a forward slash (`/`), an at symbol (`@`), or an exclamation mark (`!`).
+
+## Slash commands (`/`)
+
+Slash commands provide meta-level control over the CLI itself.
+
+- **`/bug`**
+ - **Description:** File an issue about Gemini CLI. By default, the issue is filed within the GitHub repository for Gemini CLI. The string you enter after `/bug` will become the headline for the bug being filed. The default `/bug` behavior can be modified using the `bugCommand` setting in your `.qwen/settings.json` files.
+
+- **`/chat`**
+ - **Description:** Save and resume conversation history for branching conversation state interactively, or resuming a previous state from a later session.
+ - **Sub-commands:**
+ - **`save`**
+ - **Description:** Saves the current conversation history. You must add a `` for identifying the conversation state.
+ - **Usage:** `/chat save `
+ - **`resume`**
+ - **Description:** Resumes a conversation from a previous save.
+ - **Usage:** `/chat resume `
+ - **`list`**
+ - **Description:** Lists available tags for chat state resumption.
+
+- **`/clear`**
+ - **Description:** Clear the terminal screen, including the visible session history and scrollback within the CLI. The underlying session data (for history recall) might be preserved depending on the exact implementation, but the visual display is cleared.
+ - **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action.
+
+- **`/compress`**
+ - **Description:** Replace the entire chat context with a summary. This saves on tokens used for future tasks while retaining a high level summary of what has happened.
+
+- **`/editor`**
+ - **Description:** Open a dialog for selecting supported editors.
+
+- **`/extensions`**
+ - **Description:** Lists all active extensions in the current Gemini CLI session. See [Gemini CLI Extensions](../extension.md).
+
+- **`/help`** (or **`/?`**)
+ - **Description:** Display help information about the Gemini CLI, including available commands and their usage.
+
+- **`/mcp`**
+ - **Description:** List configured Model Context Protocol (MCP) servers, their connection status, server details, and available tools.
+ - **Sub-commands:**
+ - **`desc`** or **`descriptions`**:
+ - **Description:** Show detailed descriptions for MCP servers and tools.
+ - **`nodesc`** or **`nodescriptions`**:
+ - **Description:** Hide tool descriptions, showing only the tool names.
+ - **`schema`**:
+ - **Description:** Show the full JSON schema for the tool's configured parameters.
+ - **Keyboard Shortcut:** Press **Ctrl+T** at any time to toggle between showing and hiding tool descriptions.
+
+- **`/memory`**
+ - **Description:** Manage the AI's instructional context (hierarchical memory loaded from `GEMINI.md` files).
+ - **Sub-commands:**
+ - **`add`**:
+ - **Description:** Adds the following text to the AI's memory. Usage: `/memory add `
+ - **`show`**:
+ - **Description:** Display the full, concatenated content of the current hierarchical memory that has been loaded from all `GEMINI.md` files. This lets you inspect the instructional context being provided to the Gemini model.
+ - **`refresh`**:
+ - **Description:** Reload the hierarchical instructional memory from all `GEMINI.md` files found in the configured locations (global, project/ancestors, and sub-directories). This command updates the model with the latest `GEMINI.md` content.
+ - **Note:** For more details on how `GEMINI.md` files contribute to hierarchical memory, see the [CLI Configuration documentation](./configuration.md#4-geminimd-files-hierarchical-instructional-context).
+
+- **`/restore`**
+ - **Description:** Restores the project files to the state they were in just before a tool was executed. This is particularly useful for undoing file edits made by a tool. If run without a tool call ID, it will list available checkpoints to restore from.
+ - **Usage:** `/restore [tool_call_id]`
+ - **Note:** Only available if the CLI is invoked with the `--checkpointing` option or configured via [settings](./configuration.md). See [Checkpointing documentation](../checkpointing.md) for more details.
+
+- **`/stats`**
+ - **Description:** Display detailed statistics for the current Gemini CLI session, including token usage, cached token savings (when available), and session duration. Note: Cached token information is only displayed when cached tokens are being used, which occurs with API key authentication but not with OAuth authentication at this time.
+
+- [**`/theme`**](./themes.md)
+ - **Description:** Open a dialog that lets you change the visual theme of Gemini CLI.
+
+- **`/auth`**
+ - **Description:** Open a dialog that lets you change the authentication method.
+
+- **`/about`**
+ - **Description:** Show version info. Please share this information when filing issues.
+
+- [**`/tools`**](../tools/index.md)
+ - **Description:** Display a list of tools that are currently available within Gemini CLI.
+ - **Sub-commands:**
+ - **`desc`** or **`descriptions`**:
+ - **Description:** Show detailed descriptions of each tool, including each tool's name with its full description as provided to the model.
+ - **`nodesc`** or **`nodescriptions`**:
+ - **Description:** Hide tool descriptions, showing only the tool names.
+
+- **`/privacy`**
+ - **Description:** Display the Privacy Notice and allow users to select whether they consent to the collection of their data for service improvement purposes.
+
+- **`/quit`** (or **`/exit`**)
+ - **Description:** Exit Gemini CLI.
+
+## At commands (`@`)
+
+At commands are used to include the content of files or directories as part of your prompt to Gemini. These commands include git-aware filtering.
+
+- **`@`**
+ - **Description:** Inject the content of the specified file or files into your current prompt. This is useful for asking questions about specific code, text, or collections of files.
+ - **Examples:**
+ - `@path/to/your/file.txt Explain this text.`
+ - `@src/my_project/ Summarize the code in this directory.`
+ - `What is this file about? @README.md`
+ - **Details:**
+ - If a path to a single file is provided, the content of that file is read.
+ - If a path to a directory is provided, the command attempts to read the content of files within that directory and any subdirectories.
+ - Spaces in paths should be escaped with a backslash (e.g., `@My\ Documents/file.txt`).
+ - The command uses the `read_many_files` tool internally. The content is fetched and then inserted into your query before being sent to the Gemini model.
+ - **Git-aware filtering:** By default, git-ignored files (like `node_modules/`, `dist/`, `.env`, `.git/`) are excluded. This behavior can be changed via the `fileFiltering` settings.
+ - **File types:** The command is intended for text-based files. While it might attempt to read any file, binary files or very large files might be skipped or truncated by the underlying `read_many_files` tool to ensure performance and relevance. The tool indicates if files were skipped.
+ - **Output:** The CLI will show a tool call message indicating that `read_many_files` was used, along with a message detailing the status and the path(s) that were processed.
+
+- **`@` (Lone at symbol)**
+ - **Description:** If you type a lone `@` symbol without a path, the query is passed as-is to the Gemini model. This might be useful if you are specifically talking _about_ the `@` symbol in your prompt.
+
+### Error handling for `@` commands
+
+- If the path specified after `@` is not found or is invalid, an error message will be displayed, and the query might not be sent to the Gemini model, or it will be sent without the file content.
+- If the `read_many_files` tool encounters an error (e.g., permission issues), this will also be reported.
+
+## Shell mode & passthrough commands (`!`)
+
+The `!` prefix lets you interact with your system's shell directly from within Gemini CLI.
+
+- **`!`**
+ - **Description:** Execute the given `` in your system's default shell. Any output or errors from the command are displayed in the terminal.
+ - **Examples:**
+ - `!ls -la` (executes `ls -la` and returns to Gemini CLI)
+ - `!git status` (executes `git status` and returns to Gemini CLI)
+
+- **`!` (Toggle shell mode)**
+ - **Description:** Typing `!` on its own toggles shell mode.
+ - **Entering shell mode:**
+ - When active, shell mode uses a different coloring and a "Shell Mode Indicator".
+ - While in shell mode, text you type is interpreted directly as a shell command.
+ - **Exiting shell mode:**
+ - When exited, the UI reverts to its standard appearance and normal Gemini CLI behavior resumes.
+
+- **Caution for all `!` usage:** Commands you execute in shell mode have the same permissions and impact as if you ran them directly in your terminal.
diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md
new file mode 100644
index 00000000..98b53551
--- /dev/null
+++ b/docs/cli/configuration.md
@@ -0,0 +1,470 @@
+# Gemini CLI Configuration
+
+Gemini CLI offers several ways to configure its behavior, including environment variables, command-line arguments, and settings files. This document outlines the different configuration methods and available settings.
+
+## Configuration layers
+
+Configuration is applied in the following order of precedence (lower numbers are overridden by higher numbers):
+
+1. **Default values:** Hardcoded defaults within the application.
+2. **User settings file:** Global settings for the current user.
+3. **Project settings file:** Project-specific settings.
+4. **System settings file:** System-wide settings.
+5. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files.
+6. **Command-line arguments:** Values passed when launching the CLI.
+
+## Settings files
+
+Gemini CLI uses `settings.json` files for persistent configuration. There are three locations for these files:
+
+- **User settings file:**
+ - **Location:** `~/.qwen/settings.json` (where `~` is your home directory).
+ - **Scope:** Applies to all Gemini CLI sessions for the current user.
+- **Project settings file:**
+ - **Location:** `.qwen/settings.json` within your project's root directory.
+ - **Scope:** Applies only when running Gemini CLI from that specific project. Project settings override user settings.
+- **System settings file:**
+ - **Location:** `/etc/gemini-cli/settings.json` (Linux), `C:\ProgramData\gemini-cli\settings.json` (Windows) or `/Library/Application Support/GeminiCli/settings.json` (macOS).
+ - **Scope:** Applies to all Gemini CLI sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' Gemini CLI setups.
+
+**Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`.
+
+### The `.gemini` directory in your project
+
+In addition to a project settings file, a project's `.gemini` directory can contain other project-specific files related to Gemini CLI's operation, such as:
+
+- [Custom sandbox profiles](#sandboxing) (e.g., `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`).
+
+### Available settings in `settings.json`:
+
+- **`contextFileName`** (string or array of strings):
+ - **Description:** Specifies the filename for context files (e.g., `GEMINI.md`, `AGENTS.md`). Can be a single filename or a list of accepted filenames.
+ - **Default:** `GEMINI.md`
+ - **Example:** `"contextFileName": "AGENTS.md"`
+
+- **`bugCommand`** (object):
+ - **Description:** Overrides the default URL for the `/bug` command.
+ - **Default:** `"urlTemplate": "https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}"`
+ - **Properties:**
+ - **`urlTemplate`** (string): A URL that can contain `{title}` and `{info}` placeholders.
+ - **Example:**
+ ```json
+ "bugCommand": {
+ "urlTemplate": "https://bug.example.com/new?title={title}&info={info}"
+ }
+ ```
+
+- **`fileFiltering`** (object):
+ - **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools.
+ - **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true`
+ - **Properties:**
+ - **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations.
+ - **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt.
+ - **Example:**
+ ```json
+ "fileFiltering": {
+ "respectGitIgnore": true,
+ "enableRecursiveFileSearch": false
+ }
+ ```
+
+- **`coreTools`** (array of strings):
+ - **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to be executed.
+ - **Default:** All tools available for use by the Gemini model.
+ - **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`.
+
+- **`excludeTools`** (array of strings):
+ - **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command.
+ - **Default**: No tools excluded.
+ - **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
+ - **Security Note:** Command-specific restrictions in
+ `excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands
+ that can be executed.
+
+- **`autoAccept`** (boolean):
+ - **Description:** Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe.
+ - **Default:** `false`
+ - **Example:** `"autoAccept": true`
+
+- **`theme`** (string):
+ - **Description:** Sets the visual [theme](./themes.md) for Gemini CLI.
+ - **Default:** `"Default"`
+ - **Example:** `"theme": "GitHub"`
+
+- **`sandbox`** (boolean or string):
+ - **Description:** Controls whether and how to use sandboxing for tool execution. If set to `true`, Gemini CLI uses a pre-built `gemini-cli-sandbox` Docker image. For more information, see [Sandboxing](#sandboxing).
+ - **Default:** `false`
+ - **Example:** `"sandbox": "docker"`
+
+- **`toolDiscoveryCommand`** (string):
+ - **Description:** Defines a custom shell command for discovering tools from your project. The shell command must return on `stdout` a JSON array of [function declarations](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations). Tool wrappers are optional.
+ - **Default:** Empty
+ - **Example:** `"toolDiscoveryCommand": "bin/get_tools"`
+
+- **`toolCallCommand`** (string):
+ - **Description:** Defines a custom shell command for calling a specific tool that was discovered using `toolDiscoveryCommand`. The shell command must meet the following criteria:
+ - It must take function `name` (exactly as in [function declaration](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations)) as first command line argument.
+ - It must read function arguments as JSON on `stdin`, analogous to [`functionCall.args`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functioncall).
+ - It must return function output as JSON on `stdout`, analogous to [`functionResponse.response.content`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functionresponse).
+ - **Default:** Empty
+ - **Example:** `"toolCallCommand": "bin/call_tool"`
+
+- **`mcpServers`** (object):
+ - **Description:** Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Gemini CLI attempts to connect to each configured MCP server to discover available tools. If multiple MCP servers expose a tool with the same name, the tool names will be prefixed with the server alias you defined in the configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note that the system might strip certain schema properties from MCP tool definitions for compatibility.
+ - **Default:** Empty
+ - **Properties:**
+ - **``** (object): The server parameters for the named server.
+ - `command` (string, required): The command to execute to start the MCP server.
+ - `args` (array of strings, optional): Arguments to pass to the command.
+ - `env` (object, optional): Environment variables to set for the server process.
+ - `cwd` (string, optional): The working directory in which to start the server.
+ - `timeout` (number, optional): Timeout in milliseconds for requests to this MCP server.
+ - `trust` (boolean, optional): Trust this server and bypass all tool call confirmations.
+ - **Example:**
+ ```json
+ "mcpServers": {
+ "myPythonServer": {
+ "command": "python",
+ "args": ["mcp_server.py", "--port", "8080"],
+ "cwd": "./mcp_tools/python",
+ "timeout": 5000
+ },
+ "myNodeServer": {
+ "command": "node",
+ "args": ["mcp_server.js"],
+ "cwd": "./mcp_tools/node"
+ },
+ "myDockerServer": {
+ "command": "docker",
+ "args": ["run", "-i", "--rm", "-e", "API_KEY", "ghcr.io/foo/bar"],
+ "env": {
+ "API_KEY": "$MY_API_TOKEN"
+ }
+ }
+ }
+ ```
+
+- **`checkpointing`** (object):
+ - **Description:** Configures the checkpointing feature, which allows you to save and restore conversation and file states. See the [Checkpointing documentation](../checkpointing.md) for more details.
+ - **Default:** `{"enabled": false}`
+ - **Properties:**
+ - **`enabled`** (boolean): When `true`, the `/restore` command is available.
+
+- **`preferredEditor`** (string):
+ - **Description:** Specifies the preferred editor to use for viewing diffs.
+ - **Default:** `vscode`
+ - **Example:** `"preferredEditor": "vscode"`
+
+- **`telemetry`** (object)
+ - **Description:** Configures logging and metrics collection for Gemini CLI. For more information, see [Telemetry](../telemetry.md).
+ - **Default:** `{"enabled": false, "target": "local", "otlpEndpoint": "http://localhost:4317", "logPrompts": true}`
+ - **Properties:**
+ - **`enabled`** (boolean): Whether or not telemetry is enabled.
+ - **`target`** (string): The destination for collected telemetry. Supported values are `local` and `gcp`.
+ - **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
+ - **`logPrompts`** (boolean): Whether or not to include the content of user prompts in the logs.
+ - **Example:**
+ ```json
+ "telemetry": {
+ "enabled": true,
+ "target": "local",
+ "otlpEndpoint": "http://localhost:16686",
+ "logPrompts": false
+ }
+ ```
+- **`usageStatisticsEnabled`** (boolean):
+ - **Description:** Enables or disables the collection of usage statistics. See [Usage Statistics](#usage-statistics) for more information.
+ - **Default:** `true`
+ - **Example:**
+ ```json
+ "usageStatisticsEnabled": false
+ ```
+
+- **`hideTips`** (boolean):
+ - **Description:** Enables or disables helpful tips in the CLI interface.
+ - **Default:** `false`
+ - **Example:**
+
+ ```json
+ "hideTips": true
+ ```
+
+- **`hideBanner`** (boolean):
+ - **Description:** Enables or disables the startup banner (ASCII art logo) in the CLI interface.
+ - **Default:** `false`
+ - **Example:**
+
+ ```json
+ "hideBanner": true
+ ```
+
+- **`maxSessionTurns`** (number):
+ - **Description:** Sets the maximum number of turns for a session. If the session exceeds this limit, the CLI will stop processing and start a new chat.
+ - **Default:** `-1` (unlimited)
+ - **Example:**
+ ```json
+ "maxSessionTurns": 10
+ ```
+
+- **`enableOpenAILogging`** (boolean):
+ - **Description:** Enables or disables logging of OpenAI API calls for debugging and analysis. When enabled, all requests and responses to the OpenAI API are logged to files in the `~/.qwen/logs/` directory.
+ - **Default:** `false`
+ - **Example:**
+
+ ```json
+ "enableOpenAILogging": true
+ ```
+
+### Example `settings.json`:
+
+```json
+{
+ "theme": "GitHub",
+ "sandbox": "docker",
+ "toolDiscoveryCommand": "bin/get_tools",
+ "toolCallCommand": "bin/call_tool",
+ "mcpServers": {
+ "mainServer": {
+ "command": "bin/mcp_server.py"
+ },
+ "anotherServer": {
+ "command": "node",
+ "args": ["mcp_server.js", "--verbose"]
+ }
+ },
+ "telemetry": {
+ "enabled": true,
+ "target": "local",
+ "otlpEndpoint": "http://localhost:4317",
+ "logPrompts": true
+ },
+ "usageStatisticsEnabled": true,
+ "hideTips": false,
+ "hideBanner": false,
+ "maxSessionTurns": 10,
+ "enableOpenAILogging": true
+}
+```
+
+## Shell History
+
+The CLI keeps a history of shell commands you run. To avoid conflicts between different projects, this history is stored in a project-specific directory within your user's home folder.
+
+- **Location:** `~/.qwen/tmp//shell_history`
+ - `` is a unique identifier generated from your project's root path.
+ - The history is stored in a file named `shell_history`.
+
+## Environment Variables & `.env` Files
+
+Environment variables are a common way to configure applications, especially for sensitive information like API keys or for settings that might change between environments.
+
+The CLI automatically loads environment variables from an `.env` file. The loading order is:
+
+1. `.env` file in the current working directory.
+2. If not found, it searches upwards in parent directories until it finds an `.env` file or reaches the project root (identified by a `.git` folder) or the home directory.
+3. If still not found, it looks for `~/.env` (in the user's home directory).
+
+- **`GEMINI_API_KEY`** (Required):
+ - Your API key for the Gemini API.
+ - **Crucial for operation.** The CLI will not function without it.
+ - Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env` file.
+- **`GEMINI_MODEL`**:
+ - Specifies the default Gemini model to use.
+ - Overrides the hardcoded default
+ - Example: `export GEMINI_MODEL="gemini-2.5-flash"`
+- **`GOOGLE_API_KEY`**:
+ - Your Google Cloud API key.
+ - Required for using Vertex AI in express mode.
+ - Ensure you have the necessary permissions.
+ - Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"`.
+- **`GOOGLE_CLOUD_PROJECT`**:
+ - Your Google Cloud Project ID.
+ - Required for using Code Assist or Vertex AI.
+ - If using Vertex AI, ensure you have the necessary permissions in this project.
+ - **Cloud Shell Note:** When running in a Cloud Shell environment, this variable defaults to a special project allocated for Cloud Shell users. If you have `GOOGLE_CLOUD_PROJECT` set in your global environment in Cloud Shell, it will be overridden by this default. To use a different project in Cloud Shell, you must define `GOOGLE_CLOUD_PROJECT` in a `.env` file.
+ - Example: `export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
+- **`GOOGLE_APPLICATION_CREDENTIALS`** (string):
+ - **Description:** The path to your Google Application Credentials JSON file.
+ - **Example:** `export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
+- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
+ - Your Google Cloud Project ID for Telemetry in Google Cloud
+ - Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
+- **`GOOGLE_CLOUD_LOCATION`**:
+ - Your Google Cloud Project Location (e.g., us-central1).
+ - Required for using Vertex AI in non express mode.
+ - Example: `export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"`.
+- **`GEMINI_SANDBOX`**:
+ - Alternative to the `sandbox` setting in `settings.json`.
+ - Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
+- **`SEATBELT_PROFILE`** (macOS specific):
+ - Switches the Seatbelt (`sandbox-exec`) profile on macOS.
+ - `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations.
+ - `strict`: Uses a strict profile that declines operations by default.
+ - ``: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`).
+- **`DEBUG` or `DEBUG_MODE`** (often used by underlying libraries or the CLI itself):
+ - Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting.
+- **`NO_COLOR`**:
+ - Set to any value to disable all color output in the CLI.
+- **`CLI_TITLE`**:
+ - Set to a string to customize the title of the CLI.
+- **`CODE_ASSIST_ENDPOINT`**:
+ - Specifies the endpoint for the code assist server.
+ - This is useful for development and testing.
+
+## Command-Line Arguments
+
+Arguments passed directly when running the CLI can override other configurations for that specific session.
+
+- **`--model `** (**`-m `**):
+ - Specifies the Gemini model to use for this session.
+ - Example: `npm start -- --model gemini-1.5-pro-latest`
+- **`--prompt `** (**`-p `**):
+ - Used to pass a prompt directly to the command. This invokes Gemini CLI in a non-interactive mode.
+- **`--sandbox`** (**`-s`**):
+ - Enables sandbox mode for this session.
+- **`--sandbox-image`**:
+ - Sets the sandbox image URI.
+- **`--debug`** (**`-d`**):
+ - Enables debug mode for this session, providing more verbose output.
+- **`--all-files`** (**`-a`**):
+ - If set, recursively includes all files within the current directory as context for the prompt.
+- **`--help`** (or **`-h`**):
+ - Displays help information about command-line arguments.
+- **`--show-memory-usage`**:
+ - Displays the current memory usage.
+- **`--yolo`**:
+ - Enables YOLO mode, which automatically approves all tool calls.
+- **`--telemetry`**:
+ - Enables [telemetry](../telemetry.md).
+- **`--telemetry-target`**:
+ - Sets the telemetry target. See [telemetry](../telemetry.md) for more information.
+- **`--telemetry-otlp-endpoint`**:
+ - Sets the OTLP endpoint for telemetry. See [telemetry](../telemetry.md) for more information.
+- **`--telemetry-log-prompts`**:
+ - Enables logging of prompts for telemetry. See [telemetry](../telemetry.md) for more information.
+- **`--checkpointing`**:
+ - Enables [checkpointing](./commands.md#checkpointing-commands).
+- **`--extensions `** (**`-e `**):
+ - Specifies a list of extensions to use for the session. If not provided, all available extensions are used.
+ - Use the special term `gemini -e none` to disable all extensions.
+ - Example: `gemini -e my-extension -e my-other-extension`
+- **`--list-extensions`** (**`-l`**):
+ - Lists all available extensions and exits.
+- **`--version`**:
+ - Displays the version of the CLI.
+- **`--openai-logging`**:
+ - Enables logging of OpenAI API calls for debugging and analysis. This flag overrides the `enableOpenAILogging` setting in `settings.json`.
+
+## Context Files (Hierarchical Instructional Context)
+
+While not strictly configuration for the CLI's _behavior_, context files (defaulting to `GEMINI.md` but configurable via the `contextFileName` setting) are crucial for configuring the _instructional context_ (also referred to as "memory") provided to the Gemini model. This powerful feature allows you to give project-specific instructions, coding style guides, or any relevant background information to the AI, making its responses more tailored and accurate to your needs. The CLI includes UI elements, such as an indicator in the footer showing the number of loaded context files, to keep you informed about the active context.
+
+- **Purpose:** These Markdown files contain instructions, guidelines, or context that you want the Gemini model to be aware of during your interactions. The system is designed to manage this instructional context hierarchically.
+
+### Example Context File Content (e.g., `GEMINI.md`)
+
+Here's a conceptual example of what a context file at the root of a TypeScript project might contain:
+
+```markdown
+# Project: My Awesome TypeScript Library
+
+## General Instructions:
+
+- When generating new TypeScript code, please follow the existing coding style.
+- Ensure all new functions and classes have JSDoc comments.
+- Prefer functional programming paradigms where appropriate.
+- All code should be compatible with TypeScript 5.0 and Node.js 20+.
+
+## Coding Style:
+
+- Use 2 spaces for indentation.
+- Interface names should be prefixed with `I` (e.g., `IUserService`).
+- Private class members should be prefixed with an underscore (`_`).
+- Always use strict equality (`===` and `!==`).
+
+## Specific Component: `src/api/client.ts`
+
+- This file handles all outbound API requests.
+- When adding new API call functions, ensure they include robust error handling and logging.
+- Use the existing `fetchWithRetry` utility for all GET requests.
+
+## Regarding Dependencies:
+
+- Avoid introducing new external dependencies unless absolutely necessary.
+- If a new dependency is required, please state the reason.
+```
+
+This example demonstrates how you can provide general project context, specific coding conventions, and even notes about particular files or components. The more relevant and precise your context files are, the better the AI can assist you. Project-specific context files are highly encouraged to establish conventions and context.
+
+- **Hierarchical Loading and Precedence:** The CLI implements a sophisticated hierarchical memory system by loading context files (e.g., `GEMINI.md`) from several locations. Content from files lower in this list (more specific) typically overrides or supplements content from files higher up (more general). The exact concatenation order and final context can be inspected using the `/memory show` command. The typical loading order is:
+ 1. **Global Context File:**
+ - Location: `~/.qwen/` (e.g., `~/.qwen/GEMINI.md` in your user home directory).
+ - Scope: Provides default instructions for all your projects.
+ 2. **Project Root & Ancestors Context Files:**
+ - Location: The CLI searches for the configured context file in the current working directory and then in each parent directory up to either the project root (identified by a `.git` folder) or your home directory.
+ - Scope: Provides context relevant to the entire project or a significant portion of it.
+ 3. **Sub-directory Context Files (Contextual/Local):**
+ - Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.).
+ - Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project.
+- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt to the Gemini model. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
+- **Commands for Memory Management:**
+ - Use `/memory refresh` to force a re-scan and reload of all context files from all configured locations. This updates the AI's instructional context.
+ - Use `/memory show` to display the combined instructional context currently loaded, allowing you to verify the hierarchy and content being used by the AI.
+ - See the [Commands documentation](./commands.md#memory) for full details on the `/memory` command and its sub-commands (`show` and `refresh`).
+
+By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor the Gemini CLI's responses to your specific needs and projects.
+
+## Sandboxing
+
+The Gemini CLI can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system.
+
+Sandboxing is disabled by default, but you can enable it in a few ways:
+
+- Using `--sandbox` or `-s` flag.
+- Setting `GEMINI_SANDBOX` environment variable.
+- Sandbox is enabled in `--yolo` mode by default.
+
+By default, it uses a pre-built `gemini-cli-sandbox` Docker image.
+
+For project-specific sandboxing needs, you can create a custom Dockerfile at `.qwen/sandbox.Dockerfile` in your project's root directory. This Dockerfile can be based on the base sandbox image:
+
+```dockerfile
+FROM gemini-cli-sandbox
+
+# Add your custom dependencies or configurations here
+# For example:
+# RUN apt-get update && apt-get install -y some-package
+# COPY ./my-config /app/my-config
+```
+
+When `.qwen/sandbox.Dockerfile` exists, you can use `BUILD_SANDBOX` environment variable when running Gemini CLI to automatically build the custom sandbox image:
+
+```bash
+BUILD_SANDBOX=1 gemini -s
+```
+
+## Usage Statistics
+
+To help us improve the Gemini CLI, we collect anonymized usage statistics. This data helps us understand how the CLI is used, identify common issues, and prioritize new features.
+
+**What we collect:**
+
+- **Tool Calls:** We log the names of the tools that are called, whether they succeed or fail, and how long they take to execute. We do not collect the arguments passed to the tools or any data returned by them.
+- **API Requests:** We log the Gemini model used for each request, the duration of the request, and whether it was successful. We do not collect the content of the prompts or responses.
+- **Session Information:** We collect information about the configuration of the CLI, such as the enabled tools and the approval mode.
+
+**What we DON'T collect:**
+
+- **Personally Identifiable Information (PII):** We do not collect any personal information, such as your name, email address, or API keys.
+- **Prompt and Response Content:** We do not log the content of your prompts or the responses from the Gemini model.
+- **File Content:** We do not log the content of any files that are read or written by the CLI.
+
+**How to opt out:**
+
+You can opt out of usage statistics collection at any time by setting the `usageStatisticsEnabled` property to `false` in your `settings.json` file:
+
+```json
+{
+ "usageStatisticsEnabled": false
+}
+```
diff --git a/docs/cli/index.md b/docs/cli/index.md
new file mode 100644
index 00000000..fe10f90a
--- /dev/null
+++ b/docs/cli/index.md
@@ -0,0 +1,28 @@
+# Gemini CLI
+
+Within Gemini CLI, `packages/cli` is the frontend for users to send and receive prompts with the Gemini AI model and its associated tools. For a general overview of Gemini CLI, see the [main documentation page](../index.md).
+
+## Navigating this section
+
+- **[Authentication](./authentication.md):** A guide to setting up authentication with Google's AI services.
+- **[Commands](./commands.md):** A reference for Gemini CLI commands (e.g., `/help`, `/tools`, `/theme`).
+- **[Configuration](./configuration.md):** A guide to tailoring Gemini CLI behavior using configuration files.
+- **[Token Caching](./token-caching.md):** Optimize API costs through token caching.
+- **[Themes](./themes.md)**: A guide to customizing the CLI's appearance with different themes.
+- **[Tutorials](tutorials.md)**: A tutorial showing how to use Gemini CLI to automate a development task.
+
+## Non-interactive mode
+
+Gemini CLI can be run in a non-interactive mode, which is useful for scripting and automation. In this mode, you pipe input to the CLI, it executes the command, and then it exits.
+
+The following example pipes a command to Gemini CLI from your terminal:
+
+```bash
+echo "What is fine tuning?" | gemini
+```
+
+Gemini CLI executes the command and prints the output to your terminal. Note that you can achieve the same behavior by using the `--prompt` or `-p` flag. For example:
+
+```bash
+gemini -p "What is fine tuning?"
+```
diff --git a/docs/cli/openai-auth.md b/docs/cli/openai-auth.md
new file mode 100644
index 00000000..9dd8c0ca
--- /dev/null
+++ b/docs/cli/openai-auth.md
@@ -0,0 +1,76 @@
+# OpenAI Authentication
+
+Qwen Code CLI supports OpenAI authentication for users who want to use OpenAI models instead of Google's Gemini models.
+
+## Authentication Methods
+
+### 1. Interactive Authentication (Recommended)
+
+When you first run the CLI and select OpenAI as your authentication method, you'll be prompted to enter:
+
+- **API Key**: Your OpenAI API key from [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys)
+- **Base URL**: The base URL for OpenAI API (defaults to `https://api.openai.com/v1`)
+- **Model**: The OpenAI model to use (defaults to `gpt-4o`)
+
+The CLI will guide you through each field:
+
+1. Enter your API key and press Enter
+2. Review/modify the base URL and press Enter
+3. Review/modify the model name and press Enter
+
+**Note**: You can paste your API key directly - the CLI supports paste functionality and will display the full key for verification.
+
+### 2. Command Line Arguments
+
+You can also provide the OpenAI credentials via command line arguments:
+
+```bash
+# Basic usage with API key
+qwen-code --openai-api-key "your-api-key-here"
+
+# With custom base URL
+qwen-code --openai-api-key "your-api-key-here" --openai-base-url "https://your-custom-endpoint.com/v1"
+
+# With custom model
+qwen-code --openai-api-key "your-api-key-here" --model "gpt-4-turbo"
+```
+
+### 3. Environment Variables
+
+Set the following environment variables in your shell or `.env` file:
+
+```bash
+export OPENAI_API_KEY="your-api-key-here"
+export OPENAI_BASE_URL="https://api.openai.com/v1" # Optional, defaults to this value
+export OPENAI_MODEL="gpt-4o" # Optional, defaults to gpt-4o
+```
+
+## Supported Models
+
+The CLI supports all OpenAI models that are available through the OpenAI API, including:
+
+- `gpt-4o` (default)
+- `gpt-4o-mini`
+- `gpt-4-turbo`
+- `gpt-4`
+- `gpt-3.5-turbo`
+- And other available models
+
+## Custom Endpoints
+
+You can use custom endpoints by setting the `OPENAI_BASE_URL` environment variable or using the `--openai-base-url` command line argument. This is useful for:
+
+- Using Azure OpenAI
+- Using other OpenAI-compatible APIs
+- Using local OpenAI-compatible servers
+
+## Switching Authentication Methods
+
+To switch between authentication methods, use the `/auth` command in the CLI interface.
+
+## Security Notes
+
+- API keys are stored in memory during the session
+- For persistent storage, use environment variables or `.env` files
+- Never commit API keys to version control
+- The CLI displays API keys in plain text for verification - ensure your terminal is secure
diff --git a/docs/cli/themes.md b/docs/cli/themes.md
new file mode 100644
index 00000000..226e387e
--- /dev/null
+++ b/docs/cli/themes.md
@@ -0,0 +1,85 @@
+# Themes
+
+Gemini CLI supports a variety of themes to customize its color scheme and appearance. You can change the theme to suit your preferences via the `/theme` command or `"theme":` configuration setting.
+
+## Available Themes
+
+Gemini CLI comes with a selection of pre-defined themes, which you can list using the `/theme` command within Gemini CLI:
+
+- **Dark Themes:**
+ - `ANSI`
+ - `Atom One`
+ - `Ayu`
+ - `Default`
+ - `Dracula`
+ - `GitHub`
+- **Light Themes:**
+ - `ANSI Light`
+ - `Ayu Light`
+ - `Default Light`
+ - `GitHub Light`
+ - `Google Code`
+ - `Xcode`
+
+### Changing Themes
+
+1. Enter `/theme` into Gemini CLI.
+2. A dialog or selection prompt appears, listing the available themes.
+3. Using the arrow keys, select a theme. Some interfaces might offer a live preview or highlight as you select.
+4. Confirm your selection to apply the theme.
+
+### Theme Persistence
+
+Selected themes are saved in Gemini CLI's [configuration](./configuration.md) so your preference is remembered across sessions.
+
+## Dark Themes
+
+### ANSI
+
+
+
+### Atom OneDark
+
+
+
+### Ayu
+
+
+
+### Default
+
+
+
+### Dracula
+
+
+
+### GitHub
+
+
+
+## Light Themes
+
+### ANSI Light
+
+
+
+### Ayu Light
+
+
+
+### Default Light
+
+
+
+### GitHub Light
+
+
+
+### Google Code
+
+
+
+### Xcode
+
+
diff --git a/docs/cli/token-caching.md b/docs/cli/token-caching.md
new file mode 100644
index 00000000..17e103e1
--- /dev/null
+++ b/docs/cli/token-caching.md
@@ -0,0 +1,14 @@
+# Token Caching and Cost Optimization
+
+Gemini CLI automatically optimizes API costs through token caching when using API key authentication (Gemini API key or Vertex AI). This feature reuses previous system instructions and context to reduce the number of tokens processed in subsequent requests.
+
+**Token caching is available for:**
+
+- API key users (Gemini API key)
+- Vertex AI users (with project and location setup)
+
+**Token caching is not available for:**
+
+- OAuth users (Google Personal/Enterprise accounts) - the Code Assist API does not support cached content creation at this time
+
+You can view your token usage and cached token savings using the `/stats` command. When cached tokens are available, they will be displayed in the stats output.
diff --git a/docs/cli/tutorials.md b/docs/cli/tutorials.md
new file mode 100644
index 00000000..8c43b4c4
--- /dev/null
+++ b/docs/cli/tutorials.md
@@ -0,0 +1,69 @@
+# Tutorials
+
+This page contains tutorials for interacting with Gemini CLI.
+
+## Setting up a Model Context Protocol (MCP) server
+
+> [!CAUTION]
+> Before using a third-party MCP server, ensure you trust its source and understand the tools it provides. Your use of third-party servers is at your own risk.
+
+This tutorial demonstrates how to set up a MCP server, using the [GitHub MCP server](https://github.com/github/github-mcp-server) as an example. The GitHub MCP server provides tools for interacting with GitHub repositories, such as creating issues and commenting on pull requests.
+
+### Prerequisites
+
+Before you begin, ensure you have the following installed and configured:
+
+- **Docker:** Install and run [Docker].
+- **GitHub Personal Access Token (PAT):** Create a new [classic] or [fine-grained] PAT with the necessary scopes.
+
+[Docker]: https://www.docker.com/
+[classic]: https://github.com/settings/tokens/new
+[fine-grained]: https://github.com/settings/personal-access-tokens/new
+
+### Guide
+
+#### Configure the MCP server in `settings.json`
+
+In your project's root directory, create or open the [`.qwen/settings.json` file](./configuration.md). Within the file, add the `mcpServers` configuration block, which provides instructions for how to launch the GitHub MCP server.
+
+```json
+{
+ "mcpServers": {
+ "github": {
+ "command": "docker",
+ "args": [
+ "run",
+ "-i",
+ "--rm",
+ "-e",
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
+ "ghcr.io/github/github-mcp-server"
+ ],
+ "env": {
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
+ }
+ }
+ }
+}
+```
+
+#### Set your GitHub token
+
+> [!CAUTION]
+> Using a broadly scoped personal access token that has access to personal and private repositories can lead to information from the private repository being leaked into the public repository. We recommend using a fine-grained access token that doesn't share access to both public and private repositories.
+
+Use an environment variable to store your GitHub PAT:
+
+```bash
+GITHUB_PERSONAL_ACCESS_TOKEN="pat_YourActualGitHubTokenHere"
+```
+
+Gemini CLI uses this value in the `mcpServers` configuration that you defined in the `settings.json` file.
+
+#### Launch Gemini CLI and verify the connection
+
+When you launch Gemini CLI, it automatically reads your configuration and launches the GitHub MCP server in the background. You can then use natural language prompts to ask Gemini CLI to perform GitHub actions. For example:
+
+```bash
+"get all open issues assigned to me in the 'foo/bar' repo and prioritize them"
+```
diff --git a/docs/core/index.md b/docs/core/index.md
new file mode 100644
index 00000000..035e00c7
--- /dev/null
+++ b/docs/core/index.md
@@ -0,0 +1,55 @@
+# Gemini CLI Core
+
+Gemini CLI's core package (`packages/core`) is the backend portion of Gemini CLI, handling communication with the Gemini API, managing tools, and processing requests sent from `packages/cli`. For a general overview of Gemini CLI, see the [main documentation page](../index.md).
+
+## Navigating this section
+
+- **[Core tools API](./tools-api.md):** Information on how tools are defined, registered, and used by the core.
+- **[Memory Import Processor](./memport.md):** Documentation for the modular GEMINI.md import feature using @file.md syntax.
+
+## Role of the core
+
+While the `packages/cli` portion of Gemini CLI provides the user interface, `packages/core` is responsible for:
+
+- **Gemini API interaction:** Securely communicating with the Google Gemini API, sending user prompts, and receiving model responses.
+- **Prompt engineering:** Constructing effective prompts for the Gemini model, potentially incorporating conversation history, tool definitions, and instructional context from `GEMINI.md` files.
+- **Tool management & orchestration:**
+ - Registering available tools (e.g., file system tools, shell command execution).
+ - Interpreting tool use requests from the Gemini model.
+ - Executing the requested tools with the provided arguments.
+ - Returning tool execution results to the Gemini model for further processing.
+- **Session and state management:** Keeping track of the conversation state, including history and any relevant context required for coherent interactions.
+- **Configuration:** Managing core-specific configurations, such as API key access, model selection, and tool settings.
+
+## Security considerations
+
+The core plays a vital role in security:
+
+- **API key management:** It handles the `GEMINI_API_KEY` and ensures it's used securely when communicating with the Gemini API.
+- **Tool execution:** When tools interact with the local system (e.g., `run_shell_command`), the core (and its underlying tool implementations) must do so with appropriate caution, often involving sandboxing mechanisms to prevent unintended modifications.
+
+## Chat history compression
+
+To ensure that long conversations don't exceed the token limits of the Gemini model, the core includes a chat history compression feature.
+
+When a conversation approaches the token limit for the configured model, the core automatically compresses the conversation history before sending it to the model. This compression is designed to be lossless in terms of the information conveyed, but it reduces the overall number of tokens used.
+
+You can find the token limits for each model in the [Google AI documentation](https://ai.google.dev/gemini-api/docs/models).
+
+## Model fallback
+
+Gemini CLI includes a model fallback mechanism to ensure that you can continue to use the CLI even if the default "pro" model is rate-limited.
+
+If you are using the default "pro" model and the CLI detects that you are being rate-limited, it automatically switches to the "flash" model for the current session. This allows you to continue working without interruption.
+
+## File discovery service
+
+The file discovery service is responsible for finding files in the project that are relevant to the current context. It is used by the `@` command and other tools that need to access files.
+
+## Memory discovery service
+
+The memory discovery service is responsible for finding and loading the `GEMINI.md` files that provide context to the model. It searches for these files in a hierarchical manner, starting from the current working directory and moving up to the project root and the user's home directory. It also searches in subdirectories.
+
+This allows you to have global, project-level, and component-level context files, which are all combined to provide the model with the most relevant information.
+
+You can use the [`/memory` command](../cli/commands.md) to `show`, `add`, and `refresh` the content of loaded `GEMINI.md` files.
diff --git a/docs/core/memport.md b/docs/core/memport.md
new file mode 100644
index 00000000..cc6404e0
--- /dev/null
+++ b/docs/core/memport.md
@@ -0,0 +1,175 @@
+# Memory Import Processor
+
+The Memory Import Processor is a feature that allows you to modularize your GEMINI.md files by importing content from other markdown files using the `@file.md` syntax.
+
+## Overview
+
+This feature enables you to break down large GEMINI.md files into smaller, more manageable components that can be reused across different contexts. The import processor supports both relative and absolute paths, with built-in safety features to prevent circular imports and ensure file access security.
+
+## Important Limitations
+
+**This feature only supports `.md` (markdown) files.** Attempting to import files with other extensions (like `.txt`, `.json`, etc.) will result in a warning and the import will fail.
+
+## Syntax
+
+Use the `@` symbol followed by the path to the markdown file you want to import:
+
+```markdown
+# Main GEMINI.md file
+
+This is the main content.
+
+@./components/instructions.md
+
+More content here.
+
+@./shared/configuration.md
+```
+
+## Supported Path Formats
+
+### Relative Paths
+
+- `@./file.md` - Import from the same directory
+- `@../file.md` - Import from parent directory
+- `@./components/file.md` - Import from subdirectory
+
+### Absolute Paths
+
+- `@/absolute/path/to/file.md` - Import using absolute path
+
+## Examples
+
+### Basic Import
+
+```markdown
+# My GEMINI.md
+
+Welcome to my project!
+
+@./getting-started.md
+
+## Features
+
+@./features/overview.md
+```
+
+### Nested Imports
+
+The imported files can themselves contain imports, creating a nested structure:
+
+```markdown
+# main.md
+
+@./header.md
+@./content.md
+@./footer.md
+```
+
+```markdown
+# header.md
+
+# Project Header
+
+@./shared/title.md
+```
+
+## Safety Features
+
+### Circular Import Detection
+
+The processor automatically detects and prevents circular imports:
+
+```markdown
+# file-a.md
+
+@./file-b.md
+
+# file-b.md
+
+@./file-a.md
+```
+
+### File Access Security
+
+The `validateImportPath` function ensures that imports are only allowed from specified directories, preventing access to sensitive files outside the allowed scope.
+
+### Maximum Import Depth
+
+To prevent infinite recursion, there's a configurable maximum import depth (default: 10 levels).
+
+## Error Handling
+
+### Non-MD File Attempts
+
+If you try to import a non-markdown file, you'll see a warning:
+
+```markdown
+@./instructions.txt
+```
+
+Console output:
+
+```
+[WARN] [ImportProcessor] Import processor only supports .md files. Attempting to import non-md file: ./instructions.txt. This will fail.
+```
+
+### Missing Files
+
+If a referenced file doesn't exist, the import will fail gracefully with an error comment in the output.
+
+### File Access Errors
+
+Permission issues or other file system errors are handled gracefully with appropriate error messages.
+
+## API Reference
+
+### `processImports(content, basePath, debugMode?, importState?)`
+
+Processes import statements in GEMINI.md content.
+
+**Parameters:**
+
+- `content` (string): The content to process for imports
+- `basePath` (string): The directory path where the current file is located
+- `debugMode` (boolean, optional): Whether to enable debug logging (default: false)
+- `importState` (ImportState, optional): State tracking for circular import prevention
+
+**Returns:** Promise - Processed content with imports resolved
+
+### `validateImportPath(importPath, basePath, allowedDirectories)`
+
+Validates import paths to ensure they are safe and within allowed directories.
+
+**Parameters:**
+
+- `importPath` (string): The import path to validate
+- `basePath` (string): The base directory for resolving relative paths
+- `allowedDirectories` (string[]): Array of allowed directory paths
+
+**Returns:** boolean - Whether the import path is valid
+
+## Best Practices
+
+1. **Use descriptive file names** for imported components
+2. **Keep imports shallow** - avoid deeply nested import chains
+3. **Document your structure** - maintain a clear hierarchy of imported files
+4. **Test your imports** - ensure all referenced files exist and are accessible
+5. **Use relative paths** when possible for better portability
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Import not working**: Check that the file exists and has a `.md` extension
+2. **Circular import warnings**: Review your import structure for circular references
+3. **Permission errors**: Ensure the files are readable and within allowed directories
+4. **Path resolution issues**: Use absolute paths if relative paths aren't resolving correctly
+
+### Debug Mode
+
+Enable debug mode to see detailed logging of the import process:
+
+```typescript
+const result = await processImports(content, basePath, true);
+```
diff --git a/docs/core/tools-api.md b/docs/core/tools-api.md
new file mode 100644
index 00000000..9a902129
--- /dev/null
+++ b/docs/core/tools-api.md
@@ -0,0 +1,73 @@
+# Gemini CLI Core: Tools API
+
+The Gemini CLI core (`packages/core`) features a robust system for defining, registering, and executing tools. These tools extend the capabilities of the Gemini model, allowing it to interact with the local environment, fetch web content, and perform various actions beyond simple text generation.
+
+## Core Concepts
+
+- **Tool (`tools.ts`):** An interface and base class (`BaseTool`) that defines the contract for all tools. Each tool must have:
+ - `name`: A unique internal name (used in API calls to Gemini).
+ - `displayName`: A user-friendly name.
+ - `description`: A clear explanation of what the tool does, which is provided to the Gemini model.
+ - `parameterSchema`: A JSON schema defining the parameters the tool accepts. This is crucial for the Gemini model to understand how to call the tool correctly.
+ - `validateToolParams()`: A method to validate incoming parameters.
+ - `getDescription()`: A method to provide a human-readable description of what the tool will do with specific parameters before execution.
+ - `shouldConfirmExecute()`: A method to determine if user confirmation is required before execution (e.g., for potentially destructive operations).
+ - `execute()`: The core method that performs the tool's action and returns a `ToolResult`.
+
+- **`ToolResult` (`tools.ts`):** An interface defining the structure of a tool's execution outcome:
+ - `llmContent`: The factual string content to be included in the history sent back to the LLM for context.
+ - `returnDisplay`: A user-friendly string (often Markdown) or a special object (like `FileDiff`) for display in the CLI.
+
+- **Tool Registry (`tool-registry.ts`):** A class (`ToolRegistry`) responsible for:
+ - **Registering Tools:** Holding a collection of all available built-in tools (e.g., `ReadFileTool`, `ShellTool`).
+ - **Discovering Tools:** It can also discover tools dynamically:
+ - **Command-based Discovery:** If `toolDiscoveryCommand` is configured in settings, this command is executed. It's expected to output JSON describing custom tools, which are then registered as `DiscoveredTool` instances.
+ - **MCP-based Discovery:** If `mcpServerCommand` is configured, the registry can connect to a Model Context Protocol (MCP) server to list and register tools (`DiscoveredMCPTool`).
+ - **Providing Schemas:** Exposing the `FunctionDeclaration` schemas of all registered tools to the Gemini model, so it knows what tools are available and how to use them.
+ - **Retrieving Tools:** Allowing the core to get a specific tool by name for execution.
+
+## Built-in Tools
+
+The core comes with a suite of pre-defined tools, typically found in `packages/core/src/tools/`. These include:
+
+- **File System Tools:**
+ - `LSTool` (`ls.ts`): Lists directory contents.
+ - `ReadFileTool` (`read-file.ts`): Reads the content of a single file. It takes an `absolute_path` parameter, which must be an absolute path.
+ - `WriteFileTool` (`write-file.ts`): Writes content to a file.
+ - `GrepTool` (`grep.ts`): Searches for patterns in files.
+ - `GlobTool` (`glob.ts`): Finds files matching glob patterns.
+ - `EditTool` (`edit.ts`): Performs in-place modifications to files (often requiring confirmation).
+ - `ReadManyFilesTool` (`read-many-files.ts`): Reads and concatenates content from multiple files or glob patterns (used by the `@` command in CLI).
+- **Execution Tools:**
+ - `ShellTool` (`shell.ts`): Executes arbitrary shell commands (requires careful sandboxing and user confirmation).
+- **Web Tools:**
+ - `WebFetchTool` (`web-fetch.ts`): Fetches content from a URL.
+ - `WebSearchTool` (`web-search.ts`): Performs a web search.
+- **Memory Tools:**
+ - `MemoryTool` (`memoryTool.ts`): Interacts with the AI's memory.
+
+Each of these tools extends `BaseTool` and implements the required methods for its specific functionality.
+
+## Tool Execution Flow
+
+1. **Model Request:** The Gemini model, based on the user's prompt and the provided tool schemas, decides to use a tool and returns a `FunctionCall` part in its response, specifying the tool name and arguments.
+2. **Core Receives Request:** The core parses this `FunctionCall`.
+3. **Tool Retrieval:** It looks up the requested tool in the `ToolRegistry`.
+4. **Parameter Validation:** The tool's `validateToolParams()` method is called.
+5. **Confirmation (if needed):**
+ - The tool's `shouldConfirmExecute()` method is called.
+ - If it returns details for confirmation, the core communicates this back to the CLI, which prompts the user.
+ - The user's decision (e.g., proceed, cancel) is sent back to the core.
+6. **Execution:** If validated and confirmed (or if no confirmation is needed), the core calls the tool's `execute()` method with the provided arguments and an `AbortSignal` (for potential cancellation).
+7. **Result Processing:** The `ToolResult` from `execute()` is received by the core.
+8. **Response to Model:** The `llmContent` from the `ToolResult` is packaged as a `FunctionResponse` and sent back to the Gemini model so it can continue generating a user-facing response.
+9. **Display to User:** The `returnDisplay` from the `ToolResult` is sent to the CLI to show the user what the tool did.
+
+## Extending with Custom Tools
+
+While direct programmatic registration of new tools by users isn't explicitly detailed as a primary workflow in the provided files for typical end-users, the architecture supports extension through:
+
+- **Command-based Discovery:** Advanced users or project administrators can define a `toolDiscoveryCommand` in `settings.json`. This command, when run by the Gemini CLI core, should output a JSON array of `FunctionDeclaration` objects. The core will then make these available as `DiscoveredTool` instances. The corresponding `toolCallCommand` would then be responsible for actually executing these custom tools.
+- **MCP Server(s):** For more complex scenarios, one or more MCP servers can be set up and configured via the `mcpServers` setting in `settings.json`. The Gemini CLI core can then discover and use tools exposed by these servers. As mentioned, if you have multiple MCP servers, the tool names will be prefixed with the server name from your configuration (e.g., `serverAlias__actualToolName`).
+
+This tool system provides a flexible and powerful way to augment the Gemini model's capabilities, making the Gemini CLI a versatile assistant for a wide range of tasks.
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 00000000..3287839c
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,117 @@
+# Gemini CLI Execution and Deployment
+
+This document describes how to run Gemini CLI and explains the deployment architecture that Gemini CLI uses.
+
+## Running Gemini CLI
+
+There are several ways to run Gemini CLI. The option you choose depends on how you intend to use Gemini CLI.
+
+---
+
+### 1. Standard installation (Recommended for typical users)
+
+This is the recommended way for end-users to install Gemini CLI. It involves downloading the Gemini CLI package from the NPM registry.
+
+- **Global install:**
+
+ ```bash
+ npm install -g @google/gemini-cli
+ ```
+
+ Then, run the CLI from anywhere:
+
+ ```bash
+ gemini
+ ```
+
+- **NPX execution:**
+
+ ```bash
+ # Execute the latest version from NPM without a global install
+ npx @google/gemini-cli
+ ```
+
+---
+
+### 2. Running 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"
+ ```
+
+---
+
+### 3. Running 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-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
+ ```
+
+---
+
+### 4. Running the latest Gemini CLI commit from GitHub
+
+You can run the most recently committed version of Gemini CLI directly from the GitHub repository. This is useful for testing features still in development.
+
+```bash
+# Execute the CLI directly from the main branch on GitHub
+npx https://github.com/google-gemini/gemini-cli
+```
+
+## Deployment architecture
+
+The execution methods described above are made possible by the following architectural components and processes:
+
+**NPM packages**
+
+Gemini CLI project is a monorepo that publishes two core packages to the NPM registry:
+
+- `@google/gemini-cli-core`: The backend, handling logic and tool execution.
+- `@google/gemini-cli`: The user-facing frontend.
+
+These packages are used when performing the standard installation and when running Gemini CLI from the source.
+
+**Build and packaging processes**
+
+There are two distinct build processes used, depending on the distribution channel:
+
+- **NPM publication:** For publishing to the NPM registry, the TypeScript source code in `@google/gemini-cli-core` and `@google/gemini-cli` is transpiled into standard JavaScript using the TypeScript Compiler (`tsc`). The resulting `dist/` directory is what gets published in the NPM package. This is a standard approach for TypeScript libraries.
+
+- **GitHub `npx` execution:** When running the latest version of Gemini CLI directly from GitHub, a different process is triggered by the `prepare` script in `package.json`. This script uses `esbuild` to bundle the entire application and its dependencies into a single, self-contained JavaScript file. This bundle is created on-the-fly on the user's machine and is not checked into the repository.
+
+**Docker sandbox image**
+
+The Docker-based execution method is supported by the `gemini-cli-sandbox` container image. This image is published to a container registry and contains a pre-installed, global version of Gemini CLI.
+
+## Release process
+
+The release process is automated through GitHub Actions. The release workflow performs the following actions:
+
+1. Build the NPM packages using `tsc`.
+2. Publish the NPM packages to the artifact registry.
+3. Create GitHub releases with bundled assets.
diff --git a/docs/examples/proxy-script.md b/docs/examples/proxy-script.md
new file mode 100644
index 00000000..15afc355
--- /dev/null
+++ b/docs/examples/proxy-script.md
@@ -0,0 +1,81 @@
+# Example Proxy Script
+
+The following is an example of a proxy script that can be used with the `GEMINI_SANDBOX_PROXY_COMMAND` environment variable. This script only allows `HTTPS` connections to `example.com:443` and declines all other requests.
+
+```javascript
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// Example proxy server that listens on :::8877 and only allows HTTPS connections to example.com.
+// Set `GEMINI_SANDBOX_PROXY_COMMAND=scripts/example-proxy.js` to run proxy alongside sandbox
+// Test via `curl https://example.com` inside sandbox (in shell mode or via shell tool)
+
+import http from 'http';
+import net from 'net';
+import { URL } from 'url';
+import console from 'console';
+
+const PROXY_PORT = 8877;
+const ALLOWED_DOMAINS = ['example.com', 'googleapis.com'];
+const ALLOWED_PORT = '443';
+
+const server = http.createServer((req, res) => {
+ // Deny all requests other than CONNECT for HTTPS
+ console.log(
+ `[PROXY] Denying non-CONNECT request for: ${req.method} ${req.url}`,
+ );
+ res.writeHead(405, { 'Content-Type': 'text/plain' });
+ res.end('Method Not Allowed');
+});
+
+server.on('connect', (req, clientSocket, head) => {
+ // req.url will be in the format "hostname:port" for a CONNECT request.
+ const { port, hostname } = new URL(`http://${req.url}`);
+
+ console.log(`[PROXY] Intercepted CONNECT request for: ${hostname}:${port}`);
+
+ if (
+ ALLOWED_DOMAINS.some(
+ (domain) => hostname == domain || hostname.endsWith(`.${domain}`),
+ ) &&
+ port === ALLOWED_PORT
+ ) {
+ console.log(`[PROXY] Allowing connection to ${hostname}:${port}`);
+
+ // Establish a TCP connection to the original destination.
+ const serverSocket = net.connect(port, hostname, () => {
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
+ // Create a tunnel by piping data between the client and the destination server.
+ serverSocket.write(head);
+ serverSocket.pipe(clientSocket);
+ clientSocket.pipe(serverSocket);
+ });
+
+ serverSocket.on('error', (err) => {
+ console.error(`[PROXY] Error connecting to destination: ${err.message}`);
+ clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
+ });
+ } else {
+ console.log(`[PROXY] Denying connection to ${hostname}:${port}`);
+ clientSocket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
+ }
+
+ clientSocket.on('error', (err) => {
+ // This can happen if the client hangs up.
+ console.error(`[PROXY] Client socket error: ${err.message}`);
+ });
+});
+
+server.listen(PROXY_PORT, () => {
+ const address = server.address();
+ console.log(`[PROXY] Proxy listening on ${address.address}:${address.port}`);
+ console.log(
+ `[PROXY] Allowing HTTPS connections to domains: ${ALLOWED_DOMAINS.join(', ')}`,
+ );
+});
+```
diff --git a/docs/extension.md b/docs/extension.md
new file mode 100644
index 00000000..26df8083
--- /dev/null
+++ b/docs/extension.md
@@ -0,0 +1,42 @@
+# Gemini CLI Extensions
+
+Gemini CLI supports extensions that can be used to configure and extend its functionality.
+
+## How it works
+
+On startup, Gemini CLI looks for extensions in two locations:
+
+1. `/.qwen/extensions`
+2. `/.qwen/extensions`
+
+Gemini CLI loads all extensions from both locations. If an extension with the same name exists in both locations, the extension in the workspace directory takes precedence.
+
+Within each location, individual extensions exist as a directory that contains a `gemini-extension.json` file. For example:
+
+`/.qwen/extensions/my-extension/gemini-extension.json`
+
+### `gemini-extension.json`
+
+The `gemini-extension.json` file contains the configuration for the extension. The file has the following structure:
+
+```json
+{
+ "name": "my-extension",
+ "version": "1.0.0",
+ "mcpServers": {
+ "my-server": {
+ "command": "node my-server.js"
+ }
+ },
+ "contextFileName": "GEMINI.md",
+ "excludeTools": ["run_shell_command"]
+}
+```
+
+- `name`: The name of the extension. This is used to uniquely identify the extension. This should match the name of your extension directory.
+- `version`: The version of the extension.
+- `mcpServers`: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a [`settings.json` file](./cli/configuration.md). If both an extension and a `settings.json` file configure an MCP server with the same name, the server defined in the `settings.json` file takes precedence.
+- `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the workspace. If this property is not used but a `GEMINI.md` file is present in your extension directory, then that file will be loaded.
+- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command.
+
+When Gemini CLI starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..ecdd5a52
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,38 @@
+# Welcome to Gemini CLI documentation
+
+This documentation provides a comprehensive guide to installing, using, and developing Gemini CLI. This tool lets you interact with Gemini models through a command-line interface.
+
+## Overview
+
+Gemini CLI brings the capabilities of Gemini models to your terminal in an interactive Read-Eval-Print Loop (REPL) environment. Gemini CLI consists of a client-side application (`packages/cli`) that communicates with a local server (`packages/core`), which in turn manages requests to the Gemini API and its AI models. Gemini CLI also contains a variety of tools for tasks such as performing file system operations, running shells, and web fetching, which are managed by `packages/core`.
+
+## Navigating the documentation
+
+This documentation is organized into the following sections:
+
+- **[Execution and Deployment](./deployment.md):** Information for running Gemini CLI.
+- **[Architecture Overview](./architecture.md):** Understand the high-level design of Gemini CLI, including its components and how they interact.
+- **CLI Usage:** Documentation for `packages/cli`.
+ - **[CLI Introduction](./cli/index.md):** Overview of the command-line interface.
+ - **[Commands](./cli/commands.md):** Description of available CLI commands.
+ - **[Configuration](./cli/configuration.md):** Information on configuring the CLI.
+ - **[Checkpointing](./checkpointing.md):** Documentation for the checkpointing feature.
+ - **[Extensions](./extension.md):** How to extend the CLI with new functionality.
+ - **[Telemetry](./telemetry.md):** Overview of telemetry in the CLI.
+- **Core Details:** Documentation for `packages/core`.
+ - **[Core Introduction](./core/index.md):** Overview of the core component.
+ - **[Tools API](./core/tools-api.md):** Information on how the core manages and exposes tools.
+- **Tools:**
+ - **[Tools Overview](./tools/index.md):** Overview of the available tools.
+ - **[File System Tools](./tools/file-system.md):** Documentation for the `read_file` and `write_file` tools.
+ - **[Multi-File Read Tool](./tools/multi-file.md):** Documentation for the `read_many_files` tool.
+ - **[Shell Tool](./tools/shell.md):** Documentation for the `run_shell_command` tool.
+ - **[Web Fetch Tool](./tools/web-fetch.md):** Documentation for the `web_fetch` tool.
+ - **[Web Search Tool](./tools/web-search.md):** Documentation for the `google_web_search` tool.
+ - **[Memory Tool](./tools/memory.md):** Documentation for the `save_memory` tool.
+- **[Contributing & Development Guide](../CONTRIBUTING.md):** Information for contributors and developers, including setup, building, testing, and coding conventions.
+- **[NPM Workspaces and Publishing](./npm.md):** Details on how the project's packages are managed and published.
+- **[Troubleshooting Guide](./troubleshooting.md):** Find solutions to common problems and FAQs.
+- **[Terms of Service and Privacy Notice](./tos-privacy.md):** Information on the terms of service and privacy notices applicable to your use of Gemini CLI.
+
+We hope this documentation helps you make the most of the Gemini CLI!
diff --git a/docs/integration-tests.md b/docs/integration-tests.md
new file mode 100644
index 00000000..60dfa30b
--- /dev/null
+++ b/docs/integration-tests.md
@@ -0,0 +1,141 @@
+# Integration Tests
+
+This document provides information about the integration testing framework used in this project.
+
+## Overview
+
+The integration tests are designed to validate the end-to-end functionality of the Gemini CLI. They execute the built binary in a controlled environment and verify that it behaves as expected when interacting with the file system.
+
+These tests are located in the `integration-tests` directory and are run using a custom test runner.
+
+## Running the tests
+
+The integration tests are not run as part of the default `npm run test` command. They must be run explicitly using the `npm run test:integration:all` script.
+
+The integration tests can also be run using the following shortcut:
+
+```bash
+npm run test:e2e
+```
+
+## Running a specific set of tests
+
+To run a subset of test files, you can use `npm run ....` where is either `test:e2e` or `test:integration*` and `` is any of the `.test.js` files in the `integration-tests/` directory. For example, the following command runs `list_directory.test.js` and `write_file.test.js`:
+
+```bash
+npm run test:e2e list_directory write_file
+```
+
+### Running a single test by name
+
+To run a single test by its name, use the `--test-name-pattern` flag:
+
+```bash
+npm run test:e2e -- --test-name-pattern "reads a file"
+```
+
+### Running all tests
+
+To run the entire suite of integration tests, use the following command:
+
+```bash
+npm run test:integration:all
+```
+
+### Sandbox matrix
+
+The `all` command will run tests for `no sandboxing`, `docker` and `podman`.
+Each individual type can be run using the following commands:
+
+```bash
+npm run test:integration:sandbox:none
+```
+
+```bash
+npm run test:integration:sandbox:docker
+```
+
+```bash
+npm run test:integration:sandbox:podman
+```
+
+## Diagnostics
+
+The integration test runner provides several options for diagnostics to help track down test failures.
+
+### Keeping test output
+
+You can preserve the temporary files created during a test run for inspection. This is useful for debugging issues with file system operations.
+
+To keep the test output, you can either use the `--keep-output` flag or set the `KEEP_OUTPUT` environment variable to `true`.
+
+```bash
+# Using the flag
+npm run test:integration:sandbox:none -- --keep-output
+
+# Using the environment variable
+KEEP_OUTPUT=true npm run test:integration:sandbox:none
+```
+
+When output is kept, the test runner will print the path to the unique directory for the test run.
+
+### Verbose output
+
+For more detailed debugging, the `--verbose` flag streams the real-time output from the `gemini` command to the console.
+
+```bash
+npm run test:integration:sandbox:none -- --verbose
+```
+
+When using `--verbose` and `--keep-output` in the same command, the output is streamed to the console and also saved to a log file within the test's temporary directory.
+
+The verbose output is formatted to clearly identify the source of the logs:
+
+```
+--- TEST: : ---
+... output from the gemini command ...
+--- END TEST: : ---
+```
+
+## Linting and formatting
+
+To ensure code quality and consistency, the integration test files are linted as part of the main build process. You can also manually run the linter and auto-fixer.
+
+### Running the linter
+
+To check for linting errors, run the following command:
+
+```bash
+npm run lint
+```
+
+You can include the `--fix` flag in the command to automatically fix any fixable linting errors:
+
+```bash
+npm run lint --fix
+```
+
+## Directory structure
+
+The integration tests create a unique directory for each test run inside the `.integration-tests` directory. Within this directory, a subdirectory is created for each test file, and within that, a subdirectory is created for each individual test case.
+
+This structure makes it easy to locate the artifacts for a specific test run, file, or case.
+
+```
+.integration-tests/
+โโโ /
+ โโโ .test.js/
+ โโโ /
+ โโโ output.log
+ โโโ ...other test artifacts...
+```
+
+## Continuous integration
+
+To ensure the integration tests are always run, a GitHub Actions workflow is defined in `.github/workflows/e2e.yml`. This workflow automatically runs the integration tests on every pull request and push to the `main` branch.
+
+The workflow runs the tests in different sandboxing environments to ensure Gemini CLI is tested across each:
+
+- `sandbox:none`: Runs the tests without any sandboxing.
+- `sandbox:docker`: Runs the tests in a Docker container.
+- `sandbox:podman`: Runs the tests in a Podman container.
diff --git a/docs/npm.md b/docs/npm.md
new file mode 100644
index 00000000..ed99f0b8
--- /dev/null
+++ b/docs/npm.md
@@ -0,0 +1,280 @@
+# Package Overview
+
+This monorepo contains two main packages: `@google/gemini-cli` and `@google/gemini-cli-core`.
+
+## `@google/gemini-cli`
+
+This is the main package for the Gemini CLI. It is responsible for the user interface, command parsing, and all other user-facing functionality.
+
+When this package is published, it is bundled into a single executable file. This bundle includes all of the package's dependencies, including `@google/gemini-cli-core`. This means that whether a user installs the package with `npm install -g @google/gemini-cli` or runs it directly with `npx @google/gemini-cli`, they are using this single, self-contained executable.
+
+## `@google/gemini-cli-core`
+
+This package contains the core logic for interacting with the Gemini API. It is responsible for making API requests, handling authentication, and managing the local cache.
+
+This package is not bundled. When it is published, it is published as a standard Node.js package with its own dependencies. This allows it to be used as a standalone package in other projects, if needed. All transpiled js code in the `dist` folder is included in the package.
+
+# Release Process
+
+This project follows a structured release process to ensure that all packages are versioned and published correctly. The process is designed to be as automated as possible.
+
+## How To Release
+
+Releases are managed through the [release.yml](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) GitHub Actions workflow. To perform a manual release for a patch or hotfix:
+
+1. Navigate to the **Actions** tab of the repository.
+2. Select the **Release** workflow from the list.
+3. Click the **Run workflow** dropdown button.
+4. Fill in the required inputs:
+ - **Version**: The exact version to release (e.g., `v0.2.1`).
+ - **Ref**: The branch or commit SHA to release from (defaults to `main`).
+ - **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release.
+5. Click **Run workflow**.
+
+## Nightly Releases
+
+In addition to manual releases, this project has an automated nightly release process to provide the latest "bleeding edge" version for testing and development.
+
+### Process
+
+Every night at midnight UTC, the [Release workflow](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) runs automatically on a schedule. It performs the following steps:
+
+1. Checks out the latest code from the `main` branch.
+2. Installs all dependencies.
+3. Runs the full suite of `preflight` checks and integration tests.
+4. If all tests succeed, it calculates the next nightly version number (e.g., `v0.2.1-nightly.20230101`).
+5. It then builds and publishes the packages to npm with the `nightly` dist-tag.
+6. Finally, it creates a GitHub Release for the nightly version.
+
+### Failure Handling
+
+If any step in the nightly workflow fails, it will automatically create a new issue in the repository with the labels `bug` and `nightly-failure`. The issue will contain a link to the failed workflow run for easy debugging.
+
+### How to Use the Nightly Build
+
+To install the latest nightly build, use the `@nightly` tag:
+
+```bash
+npm install -g @google/gemini-cli@nightly
+```
+
+We also run a Google cloud build called [release-docker.yml](../.gcp/release-docker.yaml). Which publishes the sandbox docker to match your release. This will also be moved to GH and combined with the main release file once service account permissions are sorted out.
+
+### After the Release
+
+After the workflow has successfully completed, you can monitor its progress in the [GitHub Actions tab](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml). Once complete, you should:
+
+1. Go to the [pull requests page](https://github.com/google-gemini/gemini-cli/pulls) of the repository.
+2. Create a new pull request from the `release/vX.Y.Z` branch to `main`.
+3. Review the pull request (it should only contain version updates in `package.json` files) and merge it. This keeps the version in `main` up-to-date.
+
+## Release Validation
+
+After pushing a new release smoke testing should be performed to ensure that the packages are working as expected. This can be done by installing the packages locally and running a set of tests to ensure that they are functioning correctly.
+
+- `npx -y @google/gemini-cli@latest --version` to validate the push worked as expected if you were not doing a rc or dev tag
+- `npx -y @google/gemini-cli@ --version` to validate the tag pushed appropriately
+- _This is destructive locally_ `npm uninstall @google/gemini-cli && npm uninstall -g @google/gemini-cli && npm cache clean --force && npm install @google/gemini-cli@`
+- Smoke testing a basic run through of exercising a few llm commands and tools is recommended to ensure that the packages are working as expected. We'll codify this more in the future.
+
+## When to merge the version change, or not?
+
+The above pattern for creating patch or hotfix releases from current or older commits leaves the repository in the following state:
+
+1. The Tag (`vX.Y.Z-patch.1`): This tag correctly points to the original commit on main
+ that contains the stable code you intended to release. This is crucial. Anyone checking
+ out this tag gets the exact code that was published.
+2. The Branch (`release-vX.Y.Z-patch.1`): This branch contains one new commit on top of the
+ tagged commit. That new commit only contains the version number change in package.json
+ (and other related files like package-lock.json).
+
+This separation is good. It keeps your main branch history clean of release-specific
+version bumps until you decide to merge them.
+
+This is the critical decision, and it depends entirely on the nature of the release.
+
+### Merge Back for Stable Patches and Hotfixes
+
+You almost always want to merge the `release-` branch back into `main` for any
+stable patch or hotfix release.
+
+- Why? The primary reason is to update the version in main's package.json. If you release
+ v1.2.1 from an older commit but never merge the version bump back, your main branch's
+ package.json will still say "version": "1.2.0". The next developer who starts work for
+ the next feature release (v1.3.0) will be branching from a codebase that has an
+ incorrect, older version number. This leads to confusion and requires manual version
+ bumping later.
+- The Process: After the release-v1.2.1 branch is created and the package is successfully
+ published, you should open a pull request to merge release-v1.2.1 into main. This PR
+ will contain just one commit: "chore: bump version to v1.2.1". It's a clean, simple
+ integration that keeps your main branch in sync with the latest released version.
+
+### Do NOT Merge Back for Pre-Releases (RC, Beta, Dev)
+
+You typically do not merge release branches for pre-releases back into `main`.
+
+- Why? Pre-release versions (e.g., v1.3.0-rc.1, v1.3.0-rc.2) are, by definition, not
+ stable and are temporary. You don't want to pollute your main branch's history with a
+ series of version bumps for release candidates. The package.json in main should reflect
+ the latest stable release version, not an RC.
+- The Process: The release-v1.3.0-rc.1 branch is created, the npm publish --tag rc happens,
+ and then... the branch has served its purpose. You can simply delete it. The code for
+ the RC is already on main (or a feature branch), so no functional code is lost. The
+ release branch was just a temporary vehicle for the version number.
+
+## Local Testing and Validation: Changes to the Packaging and Publishing Process
+
+If you need to test the release process without actually publishing to NPM or creating a public GitHub release, you can trigger the workflow manually from the GitHub UI.
+
+1. Go to the [Actions tab](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) of the repository.
+2. Click on the "Run workflow" dropdown.
+3. Leave the `dry_run` option checked (`true`).
+4. Click the "Run workflow" button.
+
+This will run the entire release process but will skip the `npm publish` and `gh release create` steps. You can inspect the workflow logs to ensure everything is working as expected.
+
+It is crucial to test any changes to the packaging and publishing process locally before committing them. This ensures that the packages will be published correctly and that they will work as expected when installed by a user.
+
+To validate your changes, you can perform a dry run of the publishing process. This will simulate the publishing process without actually publishing the packages to the npm registry.
+
+```bash
+npm_package_version=9.9.9 SANDBOX_IMAGE_REGISTRY="registry" SANDBOX_IMAGE_NAME="thename" npm run publish:npm --dry-run
+```
+
+This command will do the following:
+
+1. Build all the packages.
+2. Run all the prepublish scripts.
+3. Create the package tarballs that would be published to npm.
+4. Print a summary of the packages that would be published.
+
+You can then inspect the generated tarballs to ensure that they contain the correct files and that the `package.json` files have been updated correctly. The tarballs will be created in the root of each package's directory (e.g., `packages/cli/google-gemini-cli-0.1.6.tgz`).
+
+By performing a dry run, you can be confident that your changes to the packaging process are correct and that the packages will be published successfully.
+
+## Release Deep Dive
+
+The main goal of the release process is to take the source code from the packages/ directory, build it, and assemble a
+clean, self-contained package in a temporary `bundle` directory at the root of the project. This `bundle` directory is what
+actually gets published to NPM.
+
+Here are the key stages:
+
+Stage 1: Pre-Release Sanity Checks and Versioning
+
+- What happens: Before any files are moved, the process ensures the project is in a good state. This involves running tests,
+ linting, and type-checking (npm run preflight). The version number in the root package.json and packages/cli/package.json
+ is updated to the new release version.
+- Why: This guarantees that only high-quality, working code is released. Versioning is the first step to signify a new
+ release.
+
+Stage 2: Building the Source Code
+
+- What happens: The TypeScript source code in packages/core/src and packages/cli/src is compiled into JavaScript.
+- File movement:
+ - packages/core/src/\*_/_.ts -> compiled to -> packages/core/dist/
+ - packages/cli/src/\*_/_.ts -> compiled to -> packages/cli/dist/
+- Why: The TypeScript code written during development needs to be converted into plain JavaScript that can be run by
+ Node.js. The core package is built first as the cli package depends on it.
+
+Stage 3: Assembling the Final Publishable Package
+
+This is the most critical stage where files are moved and transformed into their final state for publishing. A temporary
+`bundle` folder is created at the project root to house the final package contents.
+
+1. The `package.json` is Transformed:
+ - What happens: The package.json from packages/cli/ is read, modified, and written into the root `bundle`/ directory.
+ - File movement: packages/cli/package.json -> (in-memory transformation) -> `bundle`/package.json
+ - Why: The final package.json must be different from the one used in development. Key changes include:
+ - Removing devDependencies.
+ - Removing workspace-specific "dependencies": { "@gemini-cli/core": "workspace:\*" } and ensuring the core code is
+ bundled directly into the final JavaScript file.
+ - Ensuring the bin, main, and files fields point to the correct locations within the final package structure.
+
+2. The JavaScript Bundle is Created:
+ - What happens: The built JavaScript from both packages/core/dist and packages/cli/dist are bundled into a single,
+ executable JavaScript file.
+ - File movement: packages/cli/dist/index.js + packages/core/dist/index.js -> (bundled by esbuild) -> `bundle`/gemini.js (or a
+ similar name).
+ - Why: This creates a single, optimized file that contains all the necessary application code. It simplifies the package
+ by removing the need for the core package to be a separate dependency on NPM, as its code is now included directly.
+
+3. Static and Supporting Files are Copied:
+ - What happens: Essential files that are not part of the source code but are required for the package to work correctly
+ or be well-described are copied into the `bundle` directory.
+ - File movement:
+ - README.md -> `bundle`/README.md
+ - LICENSE -> `bundle`/LICENSE
+ - packages/cli/src/utils/\*.sb (sandbox profiles) -> `bundle`/
+ - Why:
+ - The README.md and LICENSE are standard files that should be included in any NPM package.
+ - The sandbox profiles (.sb files) are critical runtime assets required for the CLI's sandboxing feature to
+ function. They must be located next to the final executable.
+
+Stage 4: Publishing to NPM
+
+- What happens: The npm publish command is run from inside the root `bundle` directory.
+- Why: By running npm publish from within the `bundle` directory, only the files we carefully assembled in Stage 3 are uploaded
+ to the NPM registry. This prevents any source code, test files, or development configurations from being accidentally
+ published, resulting in a clean and minimal package for users.
+
+Summary of File Flow
+
+```mermaid
+graph TD
+ subgraph "Source Files"
+ A["packages/core/src/*.ts
packages/cli/src/*.ts"]
+ B["packages/cli/package.json"]
+ C["README.md
LICENSE
packages/cli/src/utils/*.sb"]
+ end
+
+ subgraph "Process"
+ D(Build)
+ E(Transform)
+ F(Assemble)
+ G(Publish)
+ end
+
+ subgraph "Artifacts"
+ H["Bundled JS"]
+ I["Final package.json"]
+ J["bundle/"]
+ end
+
+ subgraph "Destination"
+ K["NPM Registry"]
+ end
+
+ A --> D --> H
+ B --> E --> I
+ C --> F
+ H --> F
+ I --> F
+ F --> J
+ J --> G --> K
+```
+
+This process ensures that the final published artifact is a purpose-built, clean, and efficient representation of the
+project, rather than a direct copy of the development workspace.
+
+## NPM Workspaces
+
+This project uses [NPM Workspaces](https://docs.npmjs.com/cli/v10/using-npm/workspaces) to manage the packages within this monorepo. This simplifies development by allowing us to manage dependencies and run scripts across multiple packages from the root of the project.
+
+### How it Works
+
+The root `package.json` file defines the workspaces for this project:
+
+```json
+{
+ "workspaces": ["packages/*"]
+}
+```
+
+This tells NPM that any folder inside the `packages` directory is a separate package that should be managed as part of the workspace.
+
+### Benefits of Workspaces
+
+- **Simplified Dependency Management**: Running `npm install` from the root of the project will install all dependencies for all packages in the workspace and link them together. This means you don't need to run `npm install` in each package's directory.
+- **Automatic Linking**: Packages within the workspace can depend on each other. When you run `npm install`, NPM will automatically create symlinks between the packages. This means that when you make changes to one package, the changes are immediately available to other packages that depend on it.
+- **Simplified Script Execution**: You can run scripts in any package from the root of the project using the `--workspace` flag. For example, to run the `build` script in the `cli` package, you can run `npm run build --workspace @google/gemini-cli`.
diff --git a/docs/quota-and-pricing.md b/docs/quota-and-pricing.md
new file mode 100644
index 00000000..10cd7f34
--- /dev/null
+++ b/docs/quota-and-pricing.md
@@ -0,0 +1,70 @@
+# Gemini CLI: Quotas and Pricing
+
+Your Gemini CLI quotas and pricing depend on the type of account you use to authenticate with Google. Additionally, both quotas and pricing may be calculated differently based on the model version, requests, and tokens used. A summary of model usage is available through the `/stats` command and presented on exit at the end of a session. See [privacy and terms](./tos-privacy.md) for details on Privacy policy and Terms of Service. Note: published prices are list price; additional negotiated commercial discounting may apply.
+
+This article outlines the specific quotas and pricing applicable to the Gemini CLI when using different authentication methods.
+
+## 1. Log in with Google (Gemini Code Assist Free Tier)
+
+For users who authenticate by using their Google account to access Gemini Code Assist for individuals:
+
+- **Quota:**
+ - 60 requests per minute
+ - 1000 requests per day
+ - Token usage is not applicable
+- **Cost:** Free
+- **Details:** [Gemini Code Assist Quotas](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli)
+- **Notes:** A specific quota for different models is not specified; model fallback may occur to preserve shared experience quality.
+
+## 2. Gemini API Key (Unpaid)
+
+If you are using a Gemini API key for the free tier:
+
+- **Quota:**
+ - Flash model only
+ - 10 requests per minute
+ - 250 requests per day
+- **Cost:** Free
+- **Details:** [Gemini API Rate Limits](https://ai.google.dev/gemini-api/docs/rate-limits)
+
+## 3. Gemini API Key (Paid)
+
+If you are using a Gemini API key with a paid plan:
+
+- **Quota:** Varies by pricing tier.
+- **Cost:** Varies by pricing tier and model/token usage.
+- **Details:** [Gemini API Rate Limits](https://ai.google.dev/gemini-api/docs/rate-limits), [Gemini API Pricing](https://ai.google.dev/gemini-api/docs/pricing)
+
+## 4. Login with Google (for Workspace or Licensed Code Assist users)
+
+For users of Standard or Enterprise editions of Gemini Code Assist, quotas and pricing are based on a fixed price subscription with assigned license seats:
+
+- **Standard Tier:**
+ - **Quota:** 120 requests per minute, 1500 per day
+- **Enterprise Tier:**
+ - **Quota:** 120 requests per minute, 2000 per day
+- **Cost:** Fixed price included with your Gemini for Google Workspace or Gemini Code Assist subscription.
+- **Details:** [Gemini Code Assist Quotas](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli), [Gemini Code Assist Pricing](https://cloud.google.com/products/gemini/pricing)
+- **Notes:**
+ - Specific quota for different models is not specified; model fallback may occur to preserve shared experience quality.
+ - Members of the Google Developer Program may have Gemini Code Assist licenses through their membership.
+
+## 5. Vertex AI (Express Mode)
+
+If you are using Vertex AI in Express Mode:
+
+- **Quota:** Quotas are variable and specific to your account. See the source for more details.
+- **Cost:** After your Express Mode usage is consumed and you enable billing for your project, cost is based on standard [Vertex AI Pricing](https://cloud.google.com/vertex-ai/pricing).
+- **Details:** [Vertex AI Express Mode Quotas](https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview#quotas)
+
+## 6. Vertex AI (Regular Mode)
+
+If you are using the standard Vertex AI service:
+
+- **Quota:** Governed by a dynamic shared quota system or pre-purchased provisioned throughput.
+- **Cost:** Based on model and token usage. See [Vertex AI Pricing](https://cloud.google.com/vertex-ai/pricing).
+- **Details:** [Vertex AI Dynamic Shared Quota](https://cloud.google.com/vertex-ai/generative-ai/docs/resources/dynamic-shared-quota)
+
+## 7. Google One and Ultra plans, Gemini for Workspace plans
+
+These plans currently apply only to the use of Gemini web-based products provided by Google-based experiences (for example, the Gemini web app or the Flow video editor). These plans do not apply to the API usage which powers the Gemini CLI. Supporting these plans is under active consideration for future support.
diff --git a/docs/sandbox.md b/docs/sandbox.md
new file mode 100644
index 00000000..87763685
--- /dev/null
+++ b/docs/sandbox.md
@@ -0,0 +1,135 @@
+# Sandboxing in the Gemini CLI
+
+This document provides a guide to sandboxing in the Gemini CLI, including prerequisites, quickstart, and configuration.
+
+## Prerequisites
+
+Before using sandboxing, you need to install and set up the Gemini CLI:
+
+```bash
+npm install -g @google/gemini-cli
+```
+
+To verify the installation
+
+```bash
+gemini --version
+```
+
+## Overview of sandboxing
+
+Sandboxing isolates potentially dangerous operations (such as shell commands or file modifications) from your host system, providing a security barrier between AI operations and your environment.
+
+The benefits of sandboxing include:
+
+- **Security**: Prevent accidental system damage or data loss.
+- **Isolation**: Limit file system access to project directory.
+- **Consistency**: Ensure reproducible environments across different systems.
+- **Safety**: Reduce risk when working with untrusted code or experimental commands.
+
+## Sandboxing methods
+
+Your ideal method of sandboxing may differ depending on your platform and your preferred container solution.
+
+### 1. macOS Seatbelt (macOS only)
+
+Lightweight, built-in sandboxing using `sandbox-exec`.
+
+**Default profile**: `permissive-open` - restricts writes outside project directory but allows most other operations.
+
+### 2. Container-based (Docker/Podman)
+
+Cross-platform sandboxing with complete process isolation.
+
+**Note**: Requires building the sandbox image locally or using a published image from your organization's registry.
+
+## Quickstart
+
+```bash
+# Enable sandboxing with command flag
+gemini -s -p "analyze the code structure"
+
+# Use environment variable
+export GEMINI_SANDBOX=true
+gemini -p "run the test suite"
+
+# Configure in settings.json
+{
+ "sandbox": "docker"
+}
+```
+
+## Configuration
+
+### Enable sandboxing (in order of precedence)
+
+1. **Command flag**: `-s` or `--sandbox`
+2. **Environment variable**: `GEMINI_SANDBOX=true|docker|podman|sandbox-exec`
+3. **Settings file**: `"sandbox": true` in `settings.json`
+
+### macOS Seatbelt profiles
+
+Built-in profiles (set via `SEATBELT_PROFILE` env var):
+
+- `permissive-open` (default): Write restrictions, network allowed
+- `permissive-closed`: Write restrictions, no network
+- `permissive-proxied`: Write restrictions, network via proxy
+- `restrictive-open`: Strict restrictions, network allowed
+- `restrictive-closed`: Maximum restrictions
+
+## Linux UID/GID handling
+
+The sandbox automatically handles user permissions on Linux. Override these permissions with:
+
+```bash
+export SANDBOX_SET_UID_GID=true # Force host UID/GID
+export SANDBOX_SET_UID_GID=false # Disable UID/GID mapping
+```
+
+## Troubleshooting
+
+### Common issues
+
+**"Operation not permitted"**
+
+- Operation requires access outside sandbox.
+- Try more permissive profile or add mount points.
+
+**Missing commands**
+
+- Add to custom Dockerfile.
+- Install via `sandbox.bashrc`.
+
+**Network issues**
+
+- Check sandbox profile allows network.
+- Verify proxy configuration.
+
+### Debug mode
+
+```bash
+DEBUG=1 gemini -s -p "debug command"
+```
+
+### Inspect sandbox
+
+```bash
+# Check environment
+gemini -s -p "run shell command: env | grep SANDBOX"
+
+# List mounts
+gemini -s -p "run shell command: mount | grep workspace"
+```
+
+## Security notes
+
+- Sandboxing reduces but doesn't eliminate all risks.
+- Use the most restrictive profile that allows your work.
+- Container overhead is minimal after first build.
+- GUI applications may not work in sandboxes.
+
+## Related documentation
+
+- [Configuration](./cli/configuration.md): Full configuration options.
+- [Commands](./cli/commands.md): Available commands.
+- [Troubleshooting](./troubleshooting.md): General troubleshooting.
diff --git a/docs/telemetry.md b/docs/telemetry.md
new file mode 100644
index 00000000..f4105f4b
--- /dev/null
+++ b/docs/telemetry.md
@@ -0,0 +1,238 @@
+# Gemini CLI Observability Guide
+
+Telemetry provides data about Gemini CLI's performance, health, and usage. By enabling it, you can monitor operations, debug issues, and optimize tool usage through traces, metrics, and structured logs.
+
+Gemini CLI's telemetry system is built on the **[OpenTelemetry] (OTEL)** standard, allowing you to send data to any compatible backend.
+
+[OpenTelemetry]: https://opentelemetry.io/
+
+## Enabling telemetry
+
+You can enable telemetry in multiple ways. Configuration is primarily managed via the [`.qwen/settings.json` file](./cli/configuration.md) and environment variables, but CLI flags can override these settings for a specific session.
+
+### Order of precedence
+
+The following lists the precedence for applying telemetry settings, with items listed higher having greater precedence:
+
+1. **CLI flags (for `gemini` command):**
+ - `--telemetry` / `--no-telemetry`: Overrides `telemetry.enabled`.
+ - `--telemetry-target `: Overrides `telemetry.target`.
+ - `--telemetry-otlp-endpoint `: Overrides `telemetry.otlpEndpoint`.
+ - `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`.
+
+1. **Environment variables:**
+ - `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint`.
+
+1. **Workspace settings file (`.qwen/settings.json`):** Values from the `telemetry` object in this project-specific file.
+
+1. **User settings file (`~/.qwen/settings.json`):** Values from the `telemetry` object in this global user file.
+
+1. **Defaults:** applied if not set by any of the above.
+ - `telemetry.enabled`: `false`
+ - `telemetry.target`: `local`
+ - `telemetry.otlpEndpoint`: `http://localhost:4317`
+ - `telemetry.logPrompts`: `true`
+
+**For the `npm run telemetry -- --target=` script:**
+The `--target` argument to this script _only_ overrides the `telemetry.target` for the duration and purpose of that script (i.e., choosing which collector to start). It does not permanently change your `settings.json`. The script will first look at `settings.json` for a `telemetry.target` to use as its default.
+
+### Example settings
+
+The following code can be added to your workspace (`.qwen/settings.json`) or user (`~/.qwen/settings.json`) settings to enable telemetry and send the output to Google Cloud:
+
+```json
+{
+ "telemetry": {
+ "enabled": true,
+ "target": "gcp"
+ },
+ "sandbox": false
+}
+```
+
+## Running an OTEL Collector
+
+An OTEL Collector is a service that receives, processes, and exports telemetry data.
+The CLI sends data using the OTLP/gRPC protocol.
+
+Learn more about OTEL exporter standard configuration in [documentation][otel-config-docs].
+
+[otel-config-docs]: https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
+
+### Local
+
+Use the `npm run telemetry -- --target=local` command to automate the process of setting up a local telemetry pipeline, including configuring the necessary settings in your `.qwen/settings.json` file. The underlying script installs `otelcol-contrib` (the OpenTelemetry Collector) and `jaeger` (The Jaeger UI for viewing traces). To use it:
+
+1. **Run the command**:
+ Execute the command from the root of the repository:
+
+ ```bash
+ npm run telemetry -- --target=local
+ ```
+
+ The script will:
+ - Download Jaeger and OTEL if needed.
+ - Start a local Jaeger instance.
+ - Start an OTEL collector configured to receive data from Gemini CLI.
+ - Automatically enable telemetry in your workspace settings.
+ - On exit, disable telemetry.
+
+1. **View traces**:
+ Open your web browser and navigate to **http://localhost:16686** to access the Jaeger UI. Here you can inspect detailed traces of Gemini CLI operations.
+
+1. **Inspect logs and metrics**:
+ The script redirects the OTEL collector output (which includes logs and metrics) to `~/.qwen/tmp//otel/collector.log`. The script will provide links to view and a command to tail your telemetry data (traces, metrics, logs) locally.
+
+1. **Stop the services**:
+ Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector and Jaeger services.
+
+### Google Cloud
+
+Use the `npm run telemetry -- --target=gcp` command to automate setting up a local OpenTelemetry collector that forwards data to your Google Cloud project, including configuring the necessary settings in your `.qwen/settings.json` file. The underlying script installs `otelcol-contrib`. To use it:
+
+1. **Prerequisites**:
+ - Have a Google Cloud project ID.
+ - Export the `GOOGLE_CLOUD_PROJECT` environment variable to make it available to the OTEL collector.
+ ```bash
+ export OTLP_GOOGLE_CLOUD_PROJECT="your-project-id"
+ ```
+ - Authenticate with Google Cloud (e.g., run `gcloud auth application-default login` or ensure `GOOGLE_APPLICATION_CREDENTIALS` is set).
+ - Ensure your Google Cloud account/service account has the necessary IAM roles: "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer".
+
+1. **Run the command**:
+ Execute the command from the root of the repository:
+
+ ```bash
+ npm run telemetry -- --target=gcp
+ ```
+
+ The script will:
+ - Download the `otelcol-contrib` binary if needed.
+ - Start an OTEL collector configured to receive data from Gemini CLI and export it to your specified Google Cloud project.
+ - Automatically enable telemetry and disable sandbox mode in your workspace settings (`.qwen/settings.json`).
+ - Provide direct links to view traces, metrics, and logs in your Google Cloud Console.
+ - On exit (Ctrl+C), it will attempt to restore your original telemetry and sandbox settings.
+
+1. **Run Gemini CLI:**
+ In a separate terminal, run your Gemini CLI commands. This generates telemetry data that the collector captures.
+
+1. **View telemetry in Google Cloud**:
+ Use the links provided by the script to navigate to the Google Cloud Console and view your traces, metrics, and logs.
+
+1. **Inspect local collector logs**:
+ The script redirects the local OTEL collector output to `~/.qwen/tmp//otel/collector-gcp.log`. The script provides links to view and command to tail your collector logs locally.
+
+1. **Stop the service**:
+ Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector.
+
+## Logs and metric reference
+
+The following section describes the structure of logs and metrics generated for Gemini CLI.
+
+- A `sessionId` is included as a common attribute on all logs and metrics.
+
+### Logs
+
+Logs are timestamped records of specific events. The following events are logged for Gemini CLI:
+
+- `gemini_cli.config`: This event occurs once at startup with the CLI's configuration.
+ - **Attributes**:
+ - `model` (string)
+ - `embedding_model` (string)
+ - `sandbox_enabled` (boolean)
+ - `core_tools_enabled` (string)
+ - `approval_mode` (string)
+ - `api_key_enabled` (boolean)
+ - `vertex_ai_enabled` (boolean)
+ - `code_assist_enabled` (boolean)
+ - `log_prompts_enabled` (boolean)
+ - `file_filtering_respect_git_ignore` (boolean)
+ - `debug_mode` (boolean)
+ - `mcp_servers` (string)
+
+- `gemini_cli.user_prompt`: This event occurs when a user submits a prompt.
+ - **Attributes**:
+ - `prompt_length`
+ - `prompt` (this attribute is excluded if `log_prompts_enabled` is configured to be `false`)
+ - `auth_type`
+
+- `gemini_cli.tool_call`: This event occurs for each function call.
+ - **Attributes**:
+ - `function_name`
+ - `function_args`
+ - `duration_ms`
+ - `success` (boolean)
+ - `decision` (string: "accept", "reject", or "modify", if applicable)
+ - `error` (if applicable)
+ - `error_type` (if applicable)
+
+- `gemini_cli.api_request`: This event occurs when making a request to Gemini API.
+ - **Attributes**:
+ - `model`
+ - `request_text` (if applicable)
+
+- `gemini_cli.api_error`: This event occurs if the API request fails.
+ - **Attributes**:
+ - `model`
+ - `error`
+ - `error_type`
+ - `status_code`
+ - `duration_ms`
+ - `auth_type`
+
+- `gemini_cli.api_response`: This event occurs upon receiving a response from Gemini API.
+ - **Attributes**:
+ - `model`
+ - `status_code`
+ - `duration_ms`
+ - `error` (optional)
+ - `input_token_count`
+ - `output_token_count`
+ - `cached_content_token_count`
+ - `thoughts_token_count`
+ - `tool_token_count`
+ - `response_text` (if applicable)
+ - `auth_type`
+
+- `gemini_cli.flash_fallback`: This event occurs when Gemini CLI switches to flash as fallback.
+ - **Attributes**:
+ - `auth_type`
+
+### Metrics
+
+Metrics are numerical measurements of behavior over time. The following metrics are collected for Gemini CLI:
+
+- `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup.
+
+- `gemini_cli.tool.call.count` (Counter, Int): Counts tool calls.
+ - **Attributes**:
+ - `function_name`
+ - `success` (boolean)
+ - `decision` (string: "accept", "reject", or "modify", if applicable)
+
+- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency.
+ - **Attributes**:
+ - `function_name`
+ - `decision` (string: "accept", "reject", or "modify", if applicable)
+
+- `gemini_cli.api.request.count` (Counter, Int): Counts all API requests.
+ - **Attributes**:
+ - `model`
+ - `status_code`
+ - `error_type` (if applicable)
+
+- `gemini_cli.api.request.latency` (Histogram, ms): Measures API request latency.
+ - **Attributes**:
+ - `model`
+
+- `gemini_cli.token.usage` (Counter, Int): Counts the number of tokens used.
+ - **Attributes**:
+ - `model`
+ - `type` (string: "input", "output", "thought", "cache", or "tool")
+
+- `gemini_cli.file.operation.count` (Counter, Int): Counts file operations.
+ - **Attributes**:
+ - `operation` (string: "create", "read", "update"): The type of file operation.
+ - `lines` (Int, if applicable): Number of lines in the file.
+ - `mimetype` (string, if applicable): Mimetype of the file.
+ - `extension` (string, if applicable): File extension of the file.
diff --git a/docs/tools/file-system.md b/docs/tools/file-system.md
new file mode 100644
index 00000000..05a8f512
--- /dev/null
+++ b/docs/tools/file-system.md
@@ -0,0 +1,143 @@
+# Gemini CLI file system tools
+
+The Gemini CLI provides a comprehensive suite of tools for interacting with the local file system. These tools allow the Gemini model to read from, write to, list, search, and modify files and directories, all under your control and typically with confirmation for sensitive operations.
+
+**Note:** All file system tools operate within a `rootDirectory` (usually the current working directory where you launched the CLI) for security. Paths that you provide to these tools are generally expected to be absolute or are resolved relative to this root directory.
+
+## 1. `list_directory` (ReadFolder)
+
+`list_directory` lists the names of files and subdirectories directly within a specified directory path. It can optionally ignore entries matching provided glob patterns.
+
+- **Tool name:** `list_directory`
+- **Display name:** ReadFolder
+- **File:** `ls.ts`
+- **Parameters:**
+ - `path` (string, required): The absolute path to the directory to list.
+ - `ignore` (array of strings, optional): A list of glob patterns to exclude from the listing (e.g., `["*.log", ".git"]`).
+ - `respect_git_ignore` (boolean, optional): Whether to respect `.gitignore` patterns when listing files. Defaults to `true`.
+- **Behavior:**
+ - Returns a list of file and directory names.
+ - Indicates whether each entry is a directory.
+ - Sorts entries with directories first, then alphabetically.
+- **Output (`llmContent`):** A string like: `Directory listing for /path/to/your/folder:\n[DIR] subfolder1\nfile1.txt\nfile2.png`
+- **Confirmation:** No.
+
+## 2. `read_file` (ReadFile)
+
+`read_file` reads and returns the content of a specified file. This tool handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges. Other binary file types are generally skipped.
+
+- **Tool name:** `read_file`
+- **Display name:** ReadFile
+- **File:** `read-file.ts`
+- **Parameters:**
+ - `path` (string, required): The absolute path to the file to read.
+ - `offset` (number, optional): For text files, the 0-based line number to start reading from. Requires `limit` to be set.
+ - `limit` (number, optional): For text files, the maximum number of lines to read. If omitted, reads a default maximum (e.g., 2000 lines) or the entire file if feasible.
+- **Behavior:**
+ - For text files: Returns the content. If `offset` and `limit` are used, returns only that slice of lines. Indicates if content was truncated due to line limits or line length limits.
+ - For image and PDF files: Returns the file content as a base64-encoded data structure suitable for model consumption.
+ - For other binary files: Attempts to identify and skip them, returning a message indicating it's a generic binary file.
+- **Output:** (`llmContent`):
+ - For text files: The file content, potentially prefixed with a truncation message (e.g., `[File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...`).
+ - For image/PDF files: An object containing `inlineData` with `mimeType` and base64 `data` (e.g., `{ inlineData: { mimeType: 'image/png', data: 'base64encodedstring' } }`).
+ - For other binary files: A message like `Cannot display content of binary file: /path/to/data.bin`.
+- **Confirmation:** No.
+
+## 3. `write_file` (WriteFile)
+
+`write_file` writes content to a specified file. If the file exists, it will be overwritten. If the file doesn't exist, it (and any necessary parent directories) will be created.
+
+- **Tool name:** `write_file`
+- **Display name:** WriteFile
+- **File:** `write-file.ts`
+- **Parameters:**
+ - `file_path` (string, required): The absolute path to the file to write to.
+ - `content` (string, required): The content to write into the file.
+- **Behavior:**
+ - Writes the provided `content` to the `file_path`.
+ - Creates parent directories if they don't exist.
+- **Output (`llmContent`):** A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt` or `Successfully created and wrote to new file: /path/to/new/file.txt`.
+- **Confirmation:** Yes. Shows a diff of changes and asks for user approval before writing.
+
+## 4. `glob` (FindFiles)
+
+`glob` finds files matching specific glob patterns (e.g., `src/**/*.ts`, `*.md`), returning absolute paths sorted by modification time (newest first).
+
+- **Tool name:** `glob`
+- **Display name:** FindFiles
+- **File:** `glob.ts`
+- **Parameters:**
+ - `pattern` (string, required): The glob pattern to match against (e.g., `"*.py"`, `"src/**/*.js"`).
+ - `path` (string, optional): The absolute path to the directory to search within. If omitted, searches the tool's root directory.
+ - `case_sensitive` (boolean, optional): Whether the search should be case-sensitive. Defaults to `false`.
+ - `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to `true`.
+- **Behavior:**
+ - Searches for files matching the glob pattern within the specified directory.
+ - Returns a list of absolute paths, sorted with the most recently modified files first.
+ - Ignores common nuisance directories like `node_modules` and `.git` by default.
+- **Output (`llmContent`):** A message like: `Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...`
+- **Confirmation:** No.
+
+## 5. `search_file_content` (SearchText)
+
+`search_file_content` searches for a regular expression pattern within the content of files in a specified directory. Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.
+
+- **Tool name:** `search_file_content`
+- **Display name:** SearchText
+- **File:** `grep.ts`
+- **Parameters:**
+ - `pattern` (string, required): The regular expression (regex) to search for (e.g., `"function\s+myFunction"`).
+ - `path` (string, optional): The absolute path to the directory to search within. Defaults to the current working directory.
+ - `include` (string, optional): A glob pattern to filter which files are searched (e.g., `"*.js"`, `"src/**/*.{ts,tsx}"`). If omitted, searches most files (respecting common ignores).
+- **Behavior:**
+ - Uses `git grep` if available in a Git repository for speed, otherwise falls back to system `grep` or a JavaScript-based search.
+ - Returns a list of matching lines, each prefixed with its file path (relative to the search directory) and line number.
+- **Output (`llmContent`):** A formatted string of matches, e.g.:
+ ```
+ Found 3 matches for pattern "myFunction" in path "." (filter: "*.ts"):
+ ---
+ File: src/utils.ts
+ L15: export function myFunction() {
+ L22: myFunction.call();
+ ---
+ File: src/index.ts
+ L5: import { myFunction } from './utils';
+ ---
+ ```
+- **Confirmation:** No.
+
+## 6. `replace` (Edit)
+
+`replace` replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when `expected_replacements` is specified. This tool is designed for precise, targeted changes and requires significant context around the `old_string` to ensure it modifies the correct location.
+
+- **Tool name:** `replace`
+- **Display name:** Edit
+- **File:** `edit.ts`
+- **Parameters:**
+ - `file_path` (string, required): The absolute path to the file to modify.
+ - `old_string` (string, required): The exact literal text to replace.
+
+ **CRITICAL:** This string must uniquely identify the single instance to change. It should include at least 3 lines of context _before_ and _after_ the target text, matching whitespace and indentation precisely. If `old_string` is empty, the tool attempts to create a new file at `file_path` with `new_string` as content.
+
+ - `new_string` (string, required): The exact literal text to replace `old_string` with.
+ - `expected_replacements` (number, optional): The number of occurrences to replace. Defaults to `1`.
+
+- **Behavior:**
+ - If `old_string` is empty and `file_path` does not exist, creates a new file with `new_string` as content.
+ - If `old_string` is provided, it reads the `file_path` and attempts to find exactly one occurrence of `old_string`.
+ - If one occurrence is found, it replaces it with `new_string`.
+ - **Enhanced Reliability (Multi-Stage Edit Correction):** To significantly improve the success rate of edits, especially when the model-provided `old_string` might not be perfectly precise, the tool incorporates a multi-stage edit correction mechanism.
+ - If the initial `old_string` isn't found or matches multiple locations, the tool can leverage the Gemini model to iteratively refine `old_string` (and potentially `new_string`).
+ - This self-correction process attempts to identify the unique segment the model intended to modify, making the `replace` operation more robust even with slightly imperfect initial context.
+- **Failure conditions:** Despite the correction mechanism, the tool will fail if:
+ - `file_path` is not absolute or is outside the root directory.
+ - `old_string` is not empty, but the `file_path` does not exist.
+ - `old_string` is empty, but the `file_path` already exists.
+ - `old_string` is not found in the file after attempts to correct it.
+ - `old_string` is found multiple times, and the self-correction mechanism cannot resolve it to a single, unambiguous match.
+- **Output (`llmContent`):**
+ - On success: `Successfully modified file: /path/to/file.txt (1 replacements).` or `Created new file: /path/to/new_file.txt with provided content.`
+ - On failure: An error message explaining the reason (e.g., `Failed to edit, 0 occurrences found...`, `Failed to edit, expected 1 occurrences but found 2...`).
+- **Confirmation:** Yes. Shows a diff of the proposed changes and asks for user approval before writing to the file.
+
+These file system tools provide a foundation for the Gemini CLI to understand and interact with your local project context.
diff --git a/docs/tools/index.md b/docs/tools/index.md
new file mode 100644
index 00000000..4fa98c03
--- /dev/null
+++ b/docs/tools/index.md
@@ -0,0 +1,56 @@
+# Gemini CLI tools
+
+The Gemini CLI includes built-in tools that the Gemini model uses to interact with your local environment, access information, and perform actions. These tools enhance the CLI's capabilities, enabling it to go beyond text generation and assist with a wide range of tasks.
+
+## Overview of Gemini CLI tools
+
+In the context of the Gemini CLI, tools are specific functions or modules that the Gemini model can request to be executed. For example, if you ask Gemini to "Summarize the contents of `my_document.txt`," the model will likely identify the need to read that file and will request the execution of the `read_file` tool.
+
+The core component (`packages/core`) manages these tools, presents their definitions (schemas) to the Gemini model, executes them when requested, and returns the results to the model for further processing into a user-facing response.
+
+These tools provide the following capabilities:
+
+- **Access local information:** Tools allow Gemini to access your local file system, read file contents, list directories, etc.
+- **Execute commands:** With tools like `run_shell_command`, Gemini can run shell commands (with appropriate safety measures and user confirmation).
+- **Interact with the web:** Tools can fetch content from URLs.
+- **Take actions:** Tools can modify files, write new files, or perform other actions on your system (again, typically with safeguards).
+- **Ground responses:** By using tools to fetch real-time or specific local data, Gemini's responses can be more accurate, relevant, and grounded in your actual context.
+
+## How to use Gemini CLI tools
+
+To use Gemini CLI tools, provide a prompt to the Gemini CLI. The process works as follows:
+
+1. You provide a prompt to the Gemini CLI.
+2. The CLI sends the prompt to the core.
+3. The core, along with your prompt and conversation history, sends a list of available tools and their descriptions/schemas to the Gemini API.
+4. The Gemini model analyzes your request. If it determines that a tool is needed, its response will include a request to execute a specific tool with certain parameters.
+5. The core receives this tool request, validates it, and (often after user confirmation for sensitive operations) executes the tool.
+6. The output from the tool is sent back to the Gemini model.
+7. The Gemini model uses the tool's output to formulate its final answer, which is then sent back through the core to the CLI and displayed to you.
+
+You will typically see messages in the CLI indicating when a tool is being called and whether it succeeded or failed.
+
+## Security and confirmation
+
+Many tools, especially those that can modify your file system or execute commands (`write_file`, `edit`, `run_shell_command`), are designed with safety in mind. The Gemini CLI will typically:
+
+- **Require confirmation:** Prompt you before executing potentially sensitive operations, showing you what action is about to be taken.
+- **Utilize sandboxing:** All tools are subject to restrictions enforced by sandboxing (see [Sandboxing in the Gemini CLI](../sandbox.md)). This means that when operating in a sandbox, any tools (including MCP servers) you wish to use must be available _inside_ the sandbox environment. For example, to run an MCP server through `npx`, the `npx` executable must be installed within the sandbox's Docker image or be available in the `sandbox-exec` environment.
+
+It's important to always review confirmation prompts carefully before allowing a tool to proceed.
+
+## Learn more about Gemini CLI's tools
+
+Gemini CLI's built-in tools can be broadly categorized as follows:
+
+- **[File System Tools](./file-system.md):** For interacting with files and directories (reading, writing, listing, searching, etc.).
+- **[Shell Tool](./shell.md) (`run_shell_command`):** For executing shell commands.
+- **[Web Fetch Tool](./web-fetch.md) (`web_fetch`):** For retrieving content from URLs.
+- **[Web Search Tool](./web-search.md) (`web_search`):** For searching the web.
+- **[Multi-File Read Tool](./multi-file.md) (`read_many_files`):** A specialized tool for reading content from multiple files or directories, often used by the `@` command.
+- **[Memory Tool](./memory.md) (`save_memory`):** For saving and recalling information across sessions.
+
+Additionally, these tools incorporate:
+
+- **[MCP servers](./mcp-server.md)**: MCP servers act as a bridge between the Gemini model and your local environment or other services like APIs.
+- **[Sandboxing](../sandbox.md)**: Sandboxing isolates the model and its changes from your environment to reduce potential risk.
diff --git a/docs/tools/mcp-server.md b/docs/tools/mcp-server.md
new file mode 100644
index 00000000..a481e591
--- /dev/null
+++ b/docs/tools/mcp-server.md
@@ -0,0 +1,447 @@
+# MCP servers with the Gemini CLI
+
+This document provides a guide to configuring and using Model Context Protocol (MCP) servers with the Gemini CLI.
+
+## What is an MCP server?
+
+An MCP server is an application that exposes tools and resources to the Gemini CLI through the Model Context Protocol, allowing it to interact with external systems and data sources. MCP servers act as a bridge between the Gemini model and your local environment or other services like APIs.
+
+An MCP server enables the Gemini CLI to:
+
+- **Discover tools:** List available tools, their descriptions, and parameters through standardized schema definitions.
+- **Execute tools:** Call specific tools with defined arguments and receive structured responses.
+- **Access resources:** Read data from specific resources (though the Gemini CLI primarily focuses on tool execution).
+
+With an MCP server, you can extend the Gemini CLI's capabilities to perform actions beyond its built-in features, such as interacting with databases, APIs, custom scripts, or specialized workflows.
+
+## Core Integration Architecture
+
+The Gemini CLI integrates with MCP servers through a sophisticated discovery and execution system built into the core package (`packages/core/src/tools/`):
+
+### Discovery Layer (`mcp-client.ts`)
+
+The discovery process is orchestrated by `discoverMcpTools()`, which:
+
+1. **Iterates through configured servers** from your `settings.json` `mcpServers` configuration
+2. **Establishes connections** using appropriate transport mechanisms (Stdio, SSE, or Streamable HTTP)
+3. **Fetches tool definitions** from each server using the MCP protocol
+4. **Sanitizes and validates** tool schemas for compatibility with the Gemini API
+5. **Registers tools** in the global tool registry with conflict resolution
+
+### Execution Layer (`mcp-tool.ts`)
+
+Each discovered MCP tool is wrapped in a `DiscoveredMCPTool` instance that:
+
+- **Handles confirmation logic** based on server trust settings and user preferences
+- **Manages tool execution** by calling the MCP server with proper parameters
+- **Processes responses** for both the LLM context and user display
+- **Maintains connection state** and handles timeouts
+
+### Transport Mechanisms
+
+The Gemini CLI supports three MCP transport types:
+
+- **Stdio Transport:** Spawns a subprocess and communicates via stdin/stdout
+- **SSE Transport:** Connects to Server-Sent Events endpoints
+- **Streamable HTTP Transport:** Uses HTTP streaming for communication
+
+## How to set up your MCP server
+
+The Gemini CLI uses the `mcpServers` configuration in your `settings.json` file to locate and connect to MCP servers. This configuration supports multiple servers with different transport mechanisms.
+
+### Configure the MCP server in settings.json
+
+You can configure MCP servers at the global level in the `~/.qwen/settings.json` file or in your project's root directory, create or open the `.qwen/settings.json` file. Within the file, add the `mcpServers` configuration block.
+
+### Configuration Structure
+
+Add an `mcpServers` object to your `settings.json` file:
+
+```json
+{ ...file contains other config objects
+ "mcpServers": {
+ "serverName": {
+ "command": "path/to/server",
+ "args": ["--arg1", "value1"],
+ "env": {
+ "API_KEY": "$MY_API_TOKEN"
+ },
+ "cwd": "./server-directory",
+ "timeout": 30000,
+ "trust": false
+ }
+ }
+}
+```
+
+### Configuration Properties
+
+Each server configuration supports the following properties:
+
+#### Required (one of the following)
+
+- **`command`** (string): Path to the executable for Stdio transport
+- **`url`** (string): SSE endpoint URL (e.g., `"http://localhost:8080/sse"`)
+- **`httpUrl`** (string): HTTP streaming endpoint URL
+
+#### Optional
+
+- **`args`** (string[]): Command-line arguments for Stdio transport
+- **`headers`** (object): Custom HTTP headers when using `url` or `httpUrl`
+- **`env`** (object): Environment variables for the server process. Values can reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax
+- **`cwd`** (string): Working directory for Stdio transport
+- **`timeout`** (number): Request timeout in milliseconds (default: 600,000ms = 10 minutes)
+- **`trust`** (boolean): When `true`, bypasses all tool call confirmations for this server (default: `false`)
+
+### Example Configurations
+
+#### Python MCP Server (Stdio)
+
+```json
+{
+ "mcpServers": {
+ "pythonTools": {
+ "command": "python",
+ "args": ["-m", "my_mcp_server", "--port", "8080"],
+ "cwd": "./mcp-servers/python",
+ "env": {
+ "DATABASE_URL": "$DB_CONNECTION_STRING",
+ "API_KEY": "${EXTERNAL_API_KEY}"
+ },
+ "timeout": 15000
+ }
+ }
+}
+```
+
+#### Node.js MCP Server (Stdio)
+
+```json
+{
+ "mcpServers": {
+ "nodeServer": {
+ "command": "node",
+ "args": ["dist/server.js", "--verbose"],
+ "cwd": "./mcp-servers/node",
+ "trust": true
+ }
+ }
+}
+```
+
+#### Docker-based MCP Server
+
+```json
+{
+ "mcpServers": {
+ "dockerizedServer": {
+ "command": "docker",
+ "args": [
+ "run",
+ "-i",
+ "--rm",
+ "-e",
+ "API_KEY",
+ "-v",
+ "${PWD}:/workspace",
+ "my-mcp-server:latest"
+ ],
+ "env": {
+ "API_KEY": "$EXTERNAL_SERVICE_TOKEN"
+ }
+ }
+ }
+}
+```
+
+#### HTTP-based MCP Server
+
+```json
+{
+ "mcpServers": {
+ "httpServer": {
+ "httpUrl": "http://localhost:3000/mcp",
+ "timeout": 5000
+ }
+ }
+}
+```
+
+#### HTTP-based MCP Server with Custom Headers
+
+```json
+{
+ "mcpServers": {
+ "httpServerWithAuth": {
+ "httpUrl": "http://localhost:3000/mcp",
+ "headers": {
+ "Authorization": "Bearer your-api-token",
+ "X-Custom-Header": "custom-value",
+ "Content-Type": "application/json"
+ },
+ "timeout": 5000
+ }
+ }
+}
+```
+
+## Discovery Process Deep Dive
+
+When the Gemini CLI starts, it performs MCP server discovery through the following detailed process:
+
+### 1. Server Iteration and Connection
+
+For each configured server in `mcpServers`:
+
+1. **Status tracking begins:** Server status is set to `CONNECTING`
+2. **Transport selection:** Based on configuration properties:
+ - `httpUrl` โ `StreamableHTTPClientTransport`
+ - `url` โ `SSEClientTransport`
+ - `command` โ `StdioClientTransport`
+3. **Connection establishment:** The MCP client attempts to connect with the configured timeout
+4. **Error handling:** Connection failures are logged and the server status is set to `DISCONNECTED`
+
+### 2. Tool Discovery
+
+Upon successful connection:
+
+1. **Tool listing:** The client calls the MCP server's tool listing endpoint
+2. **Schema validation:** Each tool's function declaration is validated
+3. **Name sanitization:** Tool names are cleaned to meet Gemini API requirements:
+ - Invalid characters (non-alphanumeric, underscore, dot, hyphen) are replaced with underscores
+ - Names longer than 63 characters are truncated with middle replacement (`___`)
+
+### 3. Conflict Resolution
+
+When multiple servers expose tools with the same name:
+
+1. **First registration wins:** The first server to register a tool name gets the unprefixed name
+2. **Automatic prefixing:** Subsequent servers get prefixed names: `serverName__toolName`
+3. **Registry tracking:** The tool registry maintains mappings between server names and their tools
+
+### 4. Schema Processing
+
+Tool parameter schemas undergo sanitization for Gemini API compatibility:
+
+- **`$schema` properties** are removed
+- **`additionalProperties`** are stripped
+- **`anyOf` with `default`** have their default values removed (Vertex AI compatibility)
+- **Recursive processing** applies to nested schemas
+
+### 5. Connection Management
+
+After discovery:
+
+- **Persistent connections:** Servers that successfully register tools maintain their connections
+- **Cleanup:** Servers that provide no usable tools have their connections closed
+- **Status updates:** Final server statuses are set to `CONNECTED` or `DISCONNECTED`
+
+## Tool Execution Flow
+
+When the Gemini model decides to use an MCP tool, the following execution flow occurs:
+
+### 1. Tool Invocation
+
+The model generates a `FunctionCall` with:
+
+- **Tool name:** The registered name (potentially prefixed)
+- **Arguments:** JSON object matching the tool's parameter schema
+
+### 2. Confirmation Process
+
+Each `DiscoveredMCPTool` implements sophisticated confirmation logic:
+
+#### Trust-based Bypass
+
+```typescript
+if (this.trust) {
+ return false; // No confirmation needed
+}
+```
+
+#### Dynamic Allow-listing
+
+The system maintains internal allow-lists for:
+
+- **Server-level:** `serverName` โ All tools from this server are trusted
+- **Tool-level:** `serverName.toolName` โ This specific tool is trusted
+
+#### User Choice Handling
+
+When confirmation is required, users can choose:
+
+- **Proceed once:** Execute this time only
+- **Always allow this tool:** Add to tool-level allow-list
+- **Always allow this server:** Add to server-level allow-list
+- **Cancel:** Abort execution
+
+### 3. Execution
+
+Upon confirmation (or trust bypass):
+
+1. **Parameter preparation:** Arguments are validated against the tool's schema
+2. **MCP call:** The underlying `CallableTool` invokes the server with:
+
+ ```typescript
+ const functionCalls = [
+ {
+ name: this.serverToolName, // Original server tool name
+ args: params,
+ },
+ ];
+ ```
+
+3. **Response processing:** Results are formatted for both LLM context and user display
+
+### 4. Response Handling
+
+The execution result contains:
+
+- **`llmContent`:** Raw response parts for the language model's context
+- **`returnDisplay`:** Formatted output for user display (often JSON in markdown code blocks)
+
+## How to interact with your MCP server
+
+### Using the `/mcp` Command
+
+The `/mcp` command provides comprehensive information about your MCP server setup:
+
+```bash
+/mcp
+```
+
+This displays:
+
+- **Server list:** All configured MCP servers
+- **Connection status:** `CONNECTED`, `CONNECTING`, or `DISCONNECTED`
+- **Server details:** Configuration summary (excluding sensitive data)
+- **Available tools:** List of tools from each server with descriptions
+- **Discovery state:** Overall discovery process status
+
+### Example `/mcp` Output
+
+```
+MCP Servers Status:
+
+๐ก pythonTools (CONNECTED)
+ Command: python -m my_mcp_server --port 8080
+ Working Directory: ./mcp-servers/python
+ Timeout: 15000ms
+ Tools: calculate_sum, file_analyzer, data_processor
+
+๐ nodeServer (DISCONNECTED)
+ Command: node dist/server.js --verbose
+ Error: Connection refused
+
+๐ณ dockerizedServer (CONNECTED)
+ Command: docker run -i --rm -e API_KEY my-mcp-server:latest
+ Tools: docker__deploy, docker__status
+
+Discovery State: COMPLETED
+```
+
+### Tool Usage
+
+Once discovered, MCP tools are available to the Gemini model like built-in tools. The model will automatically:
+
+1. **Select appropriate tools** based on your requests
+2. **Present confirmation dialogs** (unless the server is trusted)
+3. **Execute tools** with proper parameters
+4. **Display results** in a user-friendly format
+
+## Status Monitoring and Troubleshooting
+
+### Connection States
+
+The MCP integration tracks several states:
+
+#### Server Status (`MCPServerStatus`)
+
+- **`DISCONNECTED`:** Server is not connected or has errors
+- **`CONNECTING`:** Connection attempt in progress
+- **`CONNECTED`:** Server is connected and ready
+
+#### Discovery State (`MCPDiscoveryState`)
+
+- **`NOT_STARTED`:** Discovery hasn't begun
+- **`IN_PROGRESS`:** Currently discovering servers
+- **`COMPLETED`:** Discovery finished (with or without errors)
+
+### Common Issues and Solutions
+
+#### Server Won't Connect
+
+**Symptoms:** Server shows `DISCONNECTED` status
+
+**Troubleshooting:**
+
+1. **Check configuration:** Verify `command`, `args`, and `cwd` are correct
+2. **Test manually:** Run the server command directly to ensure it works
+3. **Check dependencies:** Ensure all required packages are installed
+4. **Review logs:** Look for error messages in the CLI output
+5. **Verify permissions:** Ensure the CLI can execute the server command
+
+#### No Tools Discovered
+
+**Symptoms:** Server connects but no tools are available
+
+**Troubleshooting:**
+
+1. **Verify tool registration:** Ensure your server actually registers tools
+2. **Check MCP protocol:** Confirm your server implements the MCP tool listing correctly
+3. **Review server logs:** Check stderr output for server-side errors
+4. **Test tool listing:** Manually test your server's tool discovery endpoint
+
+#### Tools Not Executing
+
+**Symptoms:** Tools are discovered but fail during execution
+
+**Troubleshooting:**
+
+1. **Parameter validation:** Ensure your tool accepts the expected parameters
+2. **Schema compatibility:** Verify your input schemas are valid JSON Schema
+3. **Error handling:** Check if your tool is throwing unhandled exceptions
+4. **Timeout issues:** Consider increasing the `timeout` setting
+
+#### Sandbox Compatibility
+
+**Symptoms:** MCP servers fail when sandboxing is enabled
+
+**Solutions:**
+
+1. **Docker-based servers:** Use Docker containers that include all dependencies
+2. **Path accessibility:** Ensure server executables are available in the sandbox
+3. **Network access:** Configure sandbox to allow necessary network connections
+4. **Environment variables:** Verify required environment variables are passed through
+
+### Debugging Tips
+
+1. **Enable debug mode:** Run the CLI with `--debug` for verbose output
+2. **Check stderr:** MCP server stderr is captured and logged (INFO messages filtered)
+3. **Test isolation:** Test your MCP server independently before integrating
+4. **Incremental setup:** Start with simple tools before adding complex functionality
+5. **Use `/mcp` frequently:** Monitor server status during development
+
+## Important Notes
+
+### Security Considerations
+
+- **Trust settings:** The `trust` option bypasses all confirmation dialogs. Use cautiously and only for servers you completely control
+- **Access tokens:** Be security-aware when configuring environment variables containing API keys or tokens
+- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are available within the sandbox environment
+- **Private data:** Using broadly scoped personal access tokens can lead to information leakage between repositories
+
+### Performance and Resource Management
+
+- **Connection persistence:** The CLI maintains persistent connections to servers that successfully register tools
+- **Automatic cleanup:** Connections to servers providing no tools are automatically closed
+- **Timeout management:** Configure appropriate timeouts based on your server's response characteristics
+- **Resource monitoring:** MCP servers run as separate processes and consume system resources
+
+### Schema Compatibility
+
+- **Property stripping:** The system automatically removes certain schema properties (`$schema`, `additionalProperties`) for Gemini API compatibility
+- **Name sanitization:** Tool names are automatically sanitized to meet API requirements
+- **Conflict resolution:** Tool name conflicts between servers are resolved through automatic prefixing
+
+This comprehensive integration makes MCP servers a powerful way to extend the Gemini CLI's capabilities while maintaining security, reliability, and ease of use.
diff --git a/docs/tools/memory.md b/docs/tools/memory.md
new file mode 100644
index 00000000..facd4b1c
--- /dev/null
+++ b/docs/tools/memory.md
@@ -0,0 +1,44 @@
+# Memory Tool (`save_memory`)
+
+This document describes the `save_memory` tool for the Gemini CLI.
+
+## Description
+
+Use `save_memory` to save and recall information across your Gemini CLI sessions. With `save_memory`, you can direct the CLI to remember key details across sessions, providing personalized and directed assistance.
+
+### Arguments
+
+`save_memory` takes one argument:
+
+- `fact` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement written in natural language.
+
+## How to use `save_memory` with the Gemini CLI
+
+The tool appends the provided `fact` to a special `GEMINI.md` file located in the user's home directory (`~/.qwen/GEMINI.md`). This file can be configured to have a different name.
+
+Once added, the facts are stored under a `## Gemini Added Memories` section. This file is loaded as context in subsequent sessions, allowing the CLI to recall the saved information.
+
+Usage:
+
+```
+save_memory(fact="Your fact here.")
+```
+
+### `save_memory` examples
+
+Remember a user preference:
+
+```
+save_memory(fact="My preferred programming language is Python.")
+```
+
+Store a project-specific detail:
+
+```
+save_memory(fact="The project I'm currently working on is called 'gemini-cli'.")
+```
+
+## Important notes
+
+- **General usage:** This tool should be used for concise, important facts. It is not intended for storing large amounts of data or conversational history.
+- **Memory file:** The memory file is a plain text Markdown file, so you can view and edit it manually if needed.
diff --git a/docs/tools/multi-file.md b/docs/tools/multi-file.md
new file mode 100644
index 00000000..0cd1e19e
--- /dev/null
+++ b/docs/tools/multi-file.md
@@ -0,0 +1,66 @@
+# Multi File Read Tool (`read_many_files`)
+
+This document describes the `read_many_files` tool for the Gemini CLI.
+
+## Description
+
+Use `read_many_files` to read content from multiple files specified by paths or glob patterns. The behavior of this tool depends on the provided files:
+
+- For text files, this tool concatenates their content into a single string.
+- For image (e.g., PNG, JPEG), PDF, audio (MP3, WAV), and video (MP4, MOV) files, it reads and returns them as base64-encoded data, provided they are explicitly requested by name or extension.
+
+`read_many_files` can be used to perform tasks such as getting an overview of a codebase, finding where specific functionality is implemented, reviewing documentation, or gathering context from multiple configuration files.
+
+### Arguments
+
+`read_many_files` takes the following arguments:
+
+- `paths` (list[string], required): An array of glob patterns or paths relative to the tool's target directory (e.g., `["src/**/*.ts"]`, `["README.md", "docs/", "assets/logo.png"]`).
+- `exclude` (list[string], optional): Glob patterns for files/directories to exclude (e.g., `["**/*.log", "temp/"]`). These are added to default excludes if `useDefaultExcludes` is true.
+- `include` (list[string], optional): Additional glob patterns to include. These are merged with `paths` (e.g., `["*.test.ts"]` to specifically add test files if they were broadly excluded, or `["images/*.jpg"]` to include specific image types).
+- `recursive` (boolean, optional): Whether to search recursively. This is primarily controlled by `**` in glob patterns. Defaults to `true`.
+- `useDefaultExcludes` (boolean, optional): Whether to apply a list of default exclusion patterns (e.g., `node_modules`, `.git`, non image/pdf binary files). Defaults to `true`.
+- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to true.
+
+## How to use `read_many_files` with the Gemini CLI
+
+`read_many_files` searches for files matching the provided `paths` and `include` patterns, while respecting `exclude` patterns and default excludes (if enabled).
+
+- For text files: it reads the content of each matched file (attempting to skip binary files not explicitly requested as image/PDF) and concatenates it into a single string, with a separator `--- {filePath} ---` between the content of each file. Uses UTF-8 encoding by default.
+- For image and PDF files: if explicitly requested by name or extension (e.g., `paths: ["logo.png"]` or `include: ["*.pdf"]`), the tool reads the file and returns its content as a base64 encoded string.
+- The tool attempts to detect and skip other binary files (those not matching common image/PDF types or not explicitly requested) by checking for null bytes in their initial content.
+
+Usage:
+
+```
+read_many_files(paths=["Your files or paths here."], include=["Additional files to include."], exclude=["Files to exclude."], recursive=False, useDefaultExcludes=false, respect_git_ignore=true)
+```
+
+## `read_many_files` examples
+
+Read all TypeScript files in the `src` directory:
+
+```
+read_many_files(paths=["src/**/*.ts"])
+```
+
+Read the main README, all Markdown files in the `docs` directory, and a specific logo image, excluding a specific file:
+
+```
+read_many_files(paths=["README.md", "docs/**/*.md", "assets/logo.png"], exclude=["docs/OLD_README.md"])
+```
+
+Read all JavaScript files but explicitly including test files and all JPEGs in an `images` folder:
+
+```
+read_many_files(paths=["**/*.js"], include=["**/*.test.js", "images/**/*.jpg"], useDefaultExcludes=False)
+```
+
+## Important notes
+
+- **Binary file handling:**
+ - **Image/PDF/Audio/Video files:** The tool can read common image types (PNG, JPEG, etc.), PDF, audio (mp3, wav), and video (mp4, mov) files, returning them as base64 encoded data. These files _must_ be explicitly targeted by the `paths` or `include` patterns (e.g., by specifying the exact filename like `video.mp4` or a pattern like `*.mov`).
+ - **Other binary files:** The tool attempts to detect and skip other types of binary files by examining their initial content for null bytes. The tool excludes these files from its output.
+- **Performance:** Reading a very large number of files or very large individual files can be resource-intensive.
+- **Path specificity:** Ensure paths and glob patterns are correctly specified relative to the tool's target directory. For image/PDF files, ensure the patterns are specific enough to include them.
+- **Default excludes:** Be aware of the default exclusion patterns (like `node_modules`, `.git`) and use `useDefaultExcludes=False` if you need to override them, but do so cautiously.
diff --git a/docs/tools/shell.md b/docs/tools/shell.md
new file mode 100644
index 00000000..021cede1
--- /dev/null
+++ b/docs/tools/shell.md
@@ -0,0 +1,138 @@
+# Shell Tool (`run_shell_command`)
+
+This document describes the `run_shell_command` tool for the Gemini CLI.
+
+## Description
+
+Use `run_shell_command` to interact with the underlying system, run scripts, or perform command-line operations. `run_shell_command` executes a given shell command. On Windows, the command will be executed with `cmd.exe /c`. On other platforms, the command will be executed with `bash -c`.
+
+### Arguments
+
+`run_shell_command` takes the following arguments:
+
+- `command` (string, required): The exact shell command to execute.
+- `description` (string, optional): A brief description of the command's purpose, which will be shown to the user.
+- `directory` (string, optional): The directory (relative to the project root) in which to execute the command. If not provided, the command runs in the project root.
+
+## How to use `run_shell_command` with the Gemini CLI
+
+When using `run_shell_command`, the command is executed as a subprocess. `run_shell_command` can start background processes using `&`. The tool returns detailed information about the execution, including:
+
+- `Command`: The command that was executed.
+- `Directory`: The directory where the command was run.
+- `Stdout`: Output from the standard output stream.
+- `Stderr`: Output from the standard error stream.
+- `Error`: Any error message reported by the subprocess.
+- `Exit Code`: The exit code of the command.
+- `Signal`: The signal number if the command was terminated by a signal.
+- `Background PIDs`: A list of PIDs for any background processes started.
+
+Usage:
+
+```
+run_shell_command(command="Your commands.", description="Your description of the command.", directory="Your execution directory.")
+```
+
+## `run_shell_command` examples
+
+List files in the current directory:
+
+```
+run_shell_command(command="ls -la")
+```
+
+Run a script in a specific directory:
+
+```
+run_shell_command(command="./my_script.sh", directory="scripts", description="Run my custom script")
+```
+
+Start a background server:
+
+```
+run_shell_command(command="npm run dev &", description="Start development server in background")
+```
+
+## Important notes
+
+- **Security:** Be cautious when executing commands, especially those constructed from user input, to prevent security vulnerabilities.
+- **Interactive commands:** Avoid commands that require interactive user input, as this can cause the tool to hang. Use non-interactive flags if available (e.g., `npm init -y`).
+- **Error handling:** Check the `Stderr`, `Error`, and `Exit Code` fields to determine if a command executed successfully.
+- **Background processes:** When a command is run in the background with `&`, the tool will return immediately and the process will continue to run in the background. The `Background PIDs` field will contain the process ID of the background process.
+
+## Command Restrictions
+
+You can restrict the commands that can be executed by the `run_shell_command` tool by using the `coreTools` and `excludeTools` settings in your configuration file.
+
+- `coreTools`: To restrict `run_shell_command` to a specific set of commands, add entries to the `coreTools` list in the format `run_shell_command()`. For example, `"coreTools": ["run_shell_command(git)"]` will only allow `git` commands. Including the generic `run_shell_command` acts as a wildcard, allowing any command not explicitly blocked.
+- `excludeTools`: To block specific commands, add entries to the `excludeTools` list in the format `run_shell_command()`. For example, `"excludeTools": ["run_shell_command(rm)"]` will block `rm` commands.
+
+The validation logic is designed to be secure and flexible:
+
+1. **Command Chaining Disabled**: The tool automatically splits commands chained with `&&`, `||`, or `;` and validates each part separately. If any part of the chain is disallowed, the entire command is blocked.
+2. **Prefix Matching**: The tool uses prefix matching. For example, if you allow `git`, you can run `git status` or `git log`.
+3. **Blocklist Precedence**: The `excludeTools` list is always checked first. If a command matches a blocked prefix, it will be denied, even if it also matches an allowed prefix in `coreTools`.
+
+### Command Restriction Examples
+
+**Allow only specific command prefixes**
+
+To allow only `git` and `npm` commands, and block all others:
+
+```json
+{
+ "coreTools": ["run_shell_command(git)", "run_shell_command(npm)"]
+}
+```
+
+- `git status`: Allowed
+- `npm install`: Allowed
+- `ls -l`: Blocked
+
+**Block specific command prefixes**
+
+To block `rm` and allow all other commands:
+
+```json
+{
+ "coreTools": ["run_shell_command"],
+ "excludeTools": ["run_shell_command(rm)"]
+}
+```
+
+- `rm -rf /`: Blocked
+- `git status`: Allowed
+- `npm install`: Allowed
+
+**Blocklist takes precedence**
+
+If a command prefix is in both `coreTools` and `excludeTools`, it will be blocked.
+
+```json
+{
+ "coreTools": ["run_shell_command(git)"],
+ "excludeTools": ["run_shell_command(git push)"]
+}
+```
+
+- `git push origin main`: Blocked
+- `git status`: Allowed
+
+**Block all shell commands**
+
+To block all shell commands, add the `run_shell_command` wildcard to `excludeTools`:
+
+```json
+{
+ "excludeTools": ["run_shell_command"]
+}
+```
+
+- `ls -l`: Blocked
+- `any other command`: Blocked
+
+## Security Note for `excludeTools`
+
+Command-specific restrictions in
+`excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands
+that can be executed.
diff --git a/docs/tools/web-fetch.md b/docs/tools/web-fetch.md
new file mode 100644
index 00000000..a9647df2
--- /dev/null
+++ b/docs/tools/web-fetch.md
@@ -0,0 +1,44 @@
+# Web Fetch Tool (`web_fetch`)
+
+This document describes the `web_fetch` tool for the Gemini CLI.
+
+## Description
+
+Use `web_fetch` to summarize, compare, or extract information from web pages. The `web_fetch` tool processes content from one or more URLs (up to 20) embedded in a prompt. `web_fetch` takes a natural language prompt and returns a generated response.
+
+### Arguments
+
+`web_fetch` takes one argument:
+
+- `prompt` (string, required): A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content. For example: `"Summarize https://example.com/article and extract key points from https://another.com/data"`. The prompt must contain at least one URL starting with `http://` or `https://`.
+
+## How to use `web_fetch` with the Gemini CLI
+
+To use `web_fetch` with the Gemini CLI, provide a natural language prompt that contains URLs. The tool will ask for confirmation before fetching any URLs. Once confirmed, the tool will process URLs through Gemini API's `urlContext`.
+
+If the Gemini API cannot access the URL, the tool will fall back to fetching content directly from the local machine. The tool will format the response, including source attribution and citations where possible. The tool will then provide the response to the user.
+
+Usage:
+
+```
+web_fetch(prompt="Your prompt, including a URL such as https://google.com.")
+```
+
+## `web_fetch` examples
+
+Summarize a single article:
+
+```
+web_fetch(prompt="Can you summarize the main points of https://example.com/news/latest")
+```
+
+Compare two articles:
+
+```
+web_fetch(prompt="What are the differences in the conclusions of these two papers: https://arxiv.org/abs/2401.0001 and https://arxiv.org/abs/2401.0002?")
+```
+
+## Important notes
+
+- **URL processing:** `web_fetch` relies on the Gemini API's ability to access and process the given URLs.
+- **Output quality:** The quality of the output will depend on the clarity of the instructions in the prompt.
diff --git a/docs/tools/web-search.md b/docs/tools/web-search.md
new file mode 100644
index 00000000..9ba56bfb
--- /dev/null
+++ b/docs/tools/web-search.md
@@ -0,0 +1,36 @@
+# Web Search Tool (`google_web_search`)
+
+This document describes the `google_web_search` tool.
+
+## Description
+
+Use `google_web_search` to perform a web search using Google Search via the Gemini API. The `google_web_search` tool returns a summary of web results with sources.
+
+### Arguments
+
+`google_web_search` takes one argument:
+
+- `query` (string, required): The search query.
+
+## How to use `google_web_search` with the Gemini CLI
+
+The `google_web_search` tool sends a query to the Gemini API, which then performs a web search. `google_web_search` will return a generated response based on the search results, including citations and sources.
+
+Usage:
+
+```
+google_web_search(query="Your query goes here.")
+```
+
+## `google_web_search` examples
+
+Get information on a topic:
+
+```
+google_web_search(query="latest advancements in AI-powered code generation")
+```
+
+## Important notes
+
+- **Response returned:** The `google_web_search` tool returns a processed summary, not a raw list of search results.
+- **Citations:** The response includes citations to the sources used to generate the summary.
diff --git a/docs/tos-privacy.md b/docs/tos-privacy.md
new file mode 100644
index 00000000..b2cbbc29
--- /dev/null
+++ b/docs/tos-privacy.md
@@ -0,0 +1,87 @@
+# Gemini CLI: Terms of Service and Privacy Notice
+
+Gemini CLI is an open-source tool that lets you interact with Google's powerful language models directly from your command-line interface. The Terms of Service and Privacy Notices that apply to your usage of the Gemini CLI depend on the type of account you use to authenticate with Google.
+
+This article outlines the specific terms and privacy policies applicable for different account types and authentication methods. Note: See [quotas and pricing](./quota-and-pricing.md) for the quota and pricing details that apply to your usage of the Gemini CLI.
+
+## How to determine your authentication method
+
+Your authentication method refers to the method you use to log into and access the Gemini CLI. There are four ways to authenticate:
+
+- Logging in with your Google account to Gemini Code Assist for Individuals
+- Logging in with your Google account to Gemini Code Assist for Workspace, Standard, or Enterprise Users
+- Using an API key with Gemini Developer
+- Using an API key with Vertex AI GenAI API
+
+For each of these four methods of authentication, different Terms of Service and Privacy Notices may apply.
+
+| Authentication | Account | Terms of Service | Privacy Notice |
+| :---------------------------- | :------------------ | :------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Gemini Code Assist via Google | Individual | [Google Terms of Service](https://policies.google.com/terms?hl=en-US) | [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals) |
+| Gemini Code Assist via Google | Standard/Enterprise | [Google Cloud Platform Terms of Service](https://cloud.google.com/terms) | [Gemini Code Assist Privacy Notice for Standard and Enterprise](https://cloud.google.com/gemini/docs/codeassist/security-privacy-compliance#standard_and_enterprise_data_protection_and_privacy) |
+| Gemini Developer API | Unpaid | [Gemini API Terms of Service - Unpaid Services](https://ai.google.dev/gemini-api/terms#unpaid-services) | [Google Privacy Policy](https://policies.google.com/privacy) |
+| Gemini Developer API | Paid | [Gemini API Terms of Service - Paid Services](https://ai.google.dev/gemini-api/terms#paid-services) | [Google Privacy Policy](https://policies.google.com/privacy) |
+| Vertex AI Gen API | | [Google Cloud Platform Service Terms](https://cloud.google.com/terms/service-terms/) | [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) |
+
+## 1. If you have logged in with your Google account to Gemini Code Assist for Individuals
+
+For users who use their Google account to access [Gemini Code Assist for Individuals](https://developers.google.com/gemini-code-assist/docs/overview#supported-features-gca), these Terms of Service and Privacy Notice documents apply:
+
+- **Terms of Service:** Your use of the Gemini CLI is governed by the [Google Terms of Service](https://policies.google.com/terms?hl=en-US).
+- **Privacy Notice:** The collection and use of your data is described in the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals).
+
+## 2. If you have logged in with your Google account to Gemini Code Assist for Workspace, Standard, or Enterprise Users
+
+For users who use their Google account to access the [Standard or Enterprise edition](https://cloud.google.com/gemini/docs/codeassist/overview#editions-overview) of Gemini Code Assist, these Terms of Service and Privacy Notice documents apply:
+
+- **Terms of Service:** Your use of the Gemini CLI is governed by the [Google Cloud Platform Terms of Service](https://cloud.google.com/terms).
+- **Privacy Notice:** The collection and use of your data is described in the [Gemini Code Assist Privacy Notices for Standard and Enterprise Users](https://cloud.google.com/gemini/docs/codeassist/security-privacy-compliance#standard_and_enterprise_data_protection_and_privacy).
+
+## 3. If you have logged in with a Gemini API key to the Gemini Developer API
+
+If you are using a Gemini API key for authentication with the [Gemini Developer API](https://ai.google.dev/gemini-api/docs), these Terms of Service and Privacy Notice documents apply:
+
+- **Terms of Service:** Your use of the Gemini CLI is governed by the [Gemini API Terms of Service](https://ai.google.dev/gemini-api/terms). These terms may differ depending on whether you are using an unpaid or paid service:
+ - For unpaid services, refer to the [Gemini API Terms of Service - Unpaid Services](https://ai.google.dev/gemini-api/terms#unpaid-services).
+ - For paid services, refer to the [Gemini API Terms of Service - Paid Services](https://ai.google.dev/gemini-api/terms#paid-services).
+- **Privacy Notice:** The collection and use of your data is described in the [Google Privacy Policy](https://policies.google.com/privacy).
+
+## 4. If you have logged in with a Gemini API key to the Vertex AI GenAI API
+
+If you are using a Gemini API key for authentication with a [Vertex AI GenAI API](https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest) backend, these Terms of Service and Privacy Notice documents apply:
+
+- **Terms of Service:** Your use of the Gemini CLI is governed by the [Google Cloud Platform Service Terms](https://cloud.google.com/terms/service-terms/).
+- **Privacy Notice:** The collection and use of your data is described in the [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice).
+
+### Usage Statistics Opt-Out
+
+You may opt-out from sending Usage Statistics to Google by following the instructions available here: [Usage Statistics Configuration](./cli/configuration.md#usage-statistics).
+
+## Frequently Asked Questions (FAQ) for the Gemini CLI
+
+### 1. Is my code, including prompts and answers, used to train Google's models?
+
+Whether your code, including prompts and answers, is used to train Google's models depends on the type of authentication method you use and your account type.
+
+- **Google account with Gemini Code Assist for Individuals**: Yes. When you use your personal Google account, the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals) applies. Under this notice,
+ your **prompts, answers, and related code are collected** and may be used to improve Google's products, including for model training.
+- **Google account with Gemini Code Assist for Workspace, Standard, or Enterprise**: No. For these accounts, your data is governed by the [Gemini Code Assist Privacy Notices](https://cloud.google.com/gemini/docs/codeassist/security-privacy-compliance#standard_and_enterprise_data_protection_and_privacy) terms, which treat your inputs as confidential. Your **prompts, answers, and related code are not collected** and are not used to train models.
+- **Gemini API key via the Gemini Developer API**: Whether your code is collected or used depends on whether you are using an unpaid or paid service.
+ - **Unpaid services**: Yes. When you use the Gemini API key via the Gemini Developer API with an unpaid service, the [Gemini API Terms of Service - Unpaid Services](https://ai.google.dev/gemini-api/terms#unpaid-services) terms apply. Under this notice, your **prompts, answers, and related code are collected** and may be used to improve Google's products, including for model training.
+ - **Paid services**: No. When you use the Gemini API key via the Gemini Developer API with a paid service, the [Gemini API Terms of Service - Paid Services](https://ai.google.dev/gemini-api/terms#paid-services) terms apply, which treats your inputs as confidential. Your **prompts, answers, and related code are not collected** and are not used to train models.
+- **Gemini API key via the Vertex AI GenAI API**: No. For these accounts, your data is governed by the [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) terms, which treat your inputs as confidential. Your **prompts, answers, and related code are not collected** and are not used to train models.
+
+### 2. What are Usage Statistics and what does the opt-out control?
+
+The **Usage Statistics** setting is the single control for all optional data collection in the Gemini CLI.
+
+The data it collects depends on your account and authentication type:
+
+- **Google account with Gemini Code Assist for Individuals**: When enabled, this setting allows Google to collect both anonymous telemetry (for example, commands run and performance metrics) and **your prompts and answers** for model improvement.
+- **Google account with Gemini Code Assist for Workspace, Standard, or Enterprise**: This setting only controls the collection of anonymous telemetry. Your prompts and answers are never collected, regardless of this setting.
+- **Gemini API key via the Gemini Developer API**:
+ **Unpaid services**: When enabled, this setting allows Google to collect both anonymous telemetry (like commands run and performance metrics) and **your prompts and answers** for model improvement. When disabled we will use your data as described in [How Google Uses Your Data](https://ai.google.dev/gemini-api/terms#data-use-unpaid).
+ **Paid services**: This setting only controls the collection of anonymous telemetry. Google logs prompts and responses for a limited period of time, solely for the purpose of detecting violations of the Prohibited Use Policy and any required legal or regulatory disclosures.
+- **Gemini API key via the Vertex AI GenAI API:** This setting only controls the collection of anonymous telemetry. Your prompts and answers are never collected, regardless of this setting.
+
+You can disable Usage Statistics for any account type by following the instructions in the [Usage Statistics Configuration](./cli/configuration.md#usage-statistics) documentation.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 00000000..aeafca86
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,75 @@
+# Troubleshooting Guide
+
+This guide provides solutions to common issues and debugging tips.
+
+## Authentication
+
+- **Error: `Failed to login. Message: Request contains an invalid argument`**
+ - Users with Google Workspace accounts, or users with Google Cloud accounts
+ associated with their Gmail accounts may not be able to activate the free
+ tier of the Google Code Assist plan.
+ - For Google Cloud accounts, you can work around this by setting
+ `GOOGLE_CLOUD_PROJECT` to your project ID.
+ - You can also grab an API key from [AI Studio](https://aistudio.google.com/app/apikey), which also includes a
+ separate free tier.
+
+## Frequently asked questions (FAQs)
+
+- **Q: How do I update Gemini CLI to the latest version?**
+ - A: If installed globally via npm, update Gemini CLI using the command `npm install -g @google/gemini-cli@latest`. If run from source, pull the latest changes from the repository and rebuild using `npm run build`.
+
+- **Q: Where are Gemini CLI configuration files stored?**
+ - A: The CLI configuration is stored within two `settings.json` files: one in your home directory and one in your project's root directory. In both locations, `settings.json` is found in the `.qwen/` folder. Refer to [CLI Configuration](./cli/configuration.md) for more details.
+
+- **Q: Why don't I see cached token counts in my stats output?**
+ - A: Cached token information is only displayed when cached tokens are being used. This feature is available for API key users (Gemini API key or Vertex AI) but not for OAuth users (Google Personal/Enterprise accounts) at this time, as the Code Assist API does not support cached content creation. You can still view your total token usage with the `/stats` command.
+
+## Common error messages and solutions
+
+- **Error: `EADDRINUSE` (Address already in use) when starting an MCP server.**
+ - **Cause:** Another process is already using the port the MCP server is trying to bind to.
+ - **Solution:**
+ Either stop the other process that is using the port or configure the MCP server to use a different port.
+
+- **Error: Command not found (when attempting to run Gemini CLI).**
+ - **Cause:** Gemini CLI is not correctly installed or not in your system's PATH.
+ - **Solution:**
+ 1. Ensure Gemini CLI installation was successful.
+ 2. If installed globally, check that your npm global binary directory is in your PATH.
+ 3. If running from source, ensure you are using the correct command to invoke it (e.g., `node packages/cli/dist/index.js ...`).
+
+- **Error: `MODULE_NOT_FOUND` or import errors.**
+ - **Cause:** Dependencies are not installed correctly, or the project hasn't been built.
+ - **Solution:**
+ 1. Run `npm install` to ensure all dependencies are present.
+ 2. Run `npm run build` to compile the project.
+
+- **Error: "Operation not permitted", "Permission denied", or similar.**
+ - **Cause:** If sandboxing is enabled, then the application is likely attempting an operation restricted by your sandbox, such as writing outside the project directory or system temp directory.
+ - **Solution:** See [Sandboxing](./cli/configuration.md#sandboxing) for more information, including how to customize your sandbox configuration.
+
+- **CLI is not interactive in "CI" environments**
+ - **Issue:** The CLI does not enter interactive mode (no prompt appears) if an environment variable starting with `CI_` (e.g., `CI_TOKEN`) is set. This is because the `is-in-ci` package, used by the underlying UI framework, detects these variables and assumes a non-interactive CI environment.
+ - **Cause:** The `is-in-ci` package checks for the presence of `CI`, `CONTINUOUS_INTEGRATION`, or any environment variable with a `CI_` prefix. When any of these are found, it signals that the environment is non-interactive, which prevents the CLI from starting in its interactive mode.
+ - **Solution:** If the `CI_` prefixed variable is not needed for the CLI to function, you can temporarily unset it for the command. e.g., `env -u CI_TOKEN gemini`
+
+## Debugging Tips
+
+- **CLI debugging:**
+ - Use the `--verbose` flag (if available) with CLI commands for more detailed output.
+ - Check the CLI logs, often found in a user-specific configuration or cache directory.
+
+- **Core debugging:**
+ - Check the server console output for error messages or stack traces.
+ - Increase log verbosity if configurable.
+ - Use Node.js debugging tools (e.g., `node --inspect`) if you need to step through server-side code.
+
+- **Tool issues:**
+ - If a specific tool is failing, try to isolate the issue by running the simplest possible version of the command or operation the tool performs.
+ - For `run_shell_command`, check that the command works directly in your shell first.
+ - For file system tools, double-check paths and permissions.
+
+- **Pre-flight checks:**
+ - Always run `npm run preflight` before committing code. This can catch many common issues related to formatting, linting, and type errors.
+
+If you encounter an issue not covered here, consider searching the project's issue tracker on GitHub or reporting a new issue with detailed information.
diff --git a/esbuild.config.js b/esbuild.config.js
new file mode 100644
index 00000000..abf6a998
--- /dev/null
+++ b/esbuild.config.js
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import esbuild from 'esbuild';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { createRequire } from 'module';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const require = createRequire(import.meta.url);
+const pkg = require(path.resolve(__dirname, 'package.json'));
+
+esbuild
+ .build({
+ entryPoints: ['packages/cli/index.ts'],
+ bundle: true,
+ outfile: 'bundle/gemini.js',
+ platform: 'node',
+ format: 'esm',
+ define: {
+ 'process.env.CLI_VERSION': JSON.stringify(pkg.version),
+ },
+ banner: {
+ js: `import { createRequire as _gcliCreateRequire } from 'module'; const require = _gcliCreateRequire(import.meta.url); globalThis.__filename = require('url').fileURLToPath(import.meta.url); globalThis.__dirname = require('path').dirname(globalThis.__filename);`,
+ },
+ })
+ .catch(() => process.exit(1));
diff --git a/eslint-rules/no-relative-cross-package-imports.js b/eslint-rules/no-relative-cross-package-imports.js
new file mode 100644
index 00000000..ab3ed91c
--- /dev/null
+++ b/eslint-rules/no-relative-cross-package-imports.js
@@ -0,0 +1,159 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @fileoverview Disallows relative imports between specified monorepo packages.
+ */
+'use strict';
+
+import path from 'node:path';
+import fs from 'node:fs';
+
+/**
+ * Finds the package name by searching for the nearest `package.json` file
+ * in the directory hierarchy, starting from the given file's directory
+ * and moving upwards until the specified root directory is reached.
+ * It reads the `package.json` and extracts the `name` property.
+ *
+ * @requires module:path Node.js path module
+ * @requires module:fs Node.js fs module
+ *
+ * @param {string} filePath - The path (absolute or relative) to a file within the potential package structure.
+ * The search starts from the directory containing this file.
+ * @param {string} root - The absolute path to the root directory of the project/monorepo.
+ * The upward search stops when this directory is reached.
+ * @returns {string | undefined | null} The value of the `name` field from the first `package.json` found.
+ * Returns `undefined` if the `name` field doesn't exist in the found `package.json`.
+ * Returns `null` if no `package.json` is found before reaching the `root` directory.
+ * @throws {Error} Can throw an error if `fs.readFileSync` fails (e.g., permissions) or if `JSON.parse` fails on invalid JSON content.
+ */
+function findPackageName(filePath, root) {
+ let currentDir = path.dirname(path.resolve(filePath));
+ while (currentDir !== root) {
+ const parentDir = path.dirname(currentDir);
+ const packageJsonPath = path.join(currentDir, 'package.json');
+ if (fs.existsSync(packageJsonPath)) {
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+ return pkg.name;
+ }
+
+ // Move up one level
+ currentDir = parentDir;
+ // Safety break if we somehow reached the root directly in the loop condition (less likely with path.resolve)
+ if (path.dirname(currentDir) === currentDir) break;
+ }
+
+ return null; // Not found within the expected structure
+}
+
+export default {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'Disallow relative imports between packages.',
+ category: 'Best Practices',
+ recommended: 'error',
+ },
+ fixable: 'code',
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ root: {
+ type: 'string',
+ description:
+ 'Absolute path to the root of all relevant packages to consider.',
+ },
+ },
+ required: ['root'],
+ additionalProperties: false,
+ },
+ ],
+ messages: {
+ noRelativePathsForCrossPackageImport:
+ "Relative import '{{importedPath}}' crosses package boundary from '{{importingPackage}}' to '{{importedPackage}}'. Use a direct package import ('{{importedPackage}}') instead.",
+ relativeImportIsInvalidPackage:
+ "Relative import '{{importedPath}}' does not reference a valid package. All source must be in a package directory.",
+ },
+ },
+
+ create(context) {
+ const options = context.options[0] || {};
+ const allPackagesRoot = options.root;
+
+ const currentFilePath = context.filename;
+ if (
+ !currentFilePath ||
+ currentFilePath === '' ||
+ currentFilePath === ''
+ ) {
+ // Skip if filename is not available (e.g., linting raw text)
+ return {};
+ }
+
+ const currentPackage = findPackageName(currentFilePath, allPackagesRoot);
+
+ // If the current file isn't inside a package structure, don't apply the rule
+ if (!currentPackage) {
+ return {};
+ }
+
+ return {
+ ImportDeclaration(node) {
+ const importingPackage = currentPackage;
+ const importedPath = node.source.value;
+
+ // Only interested in relative paths
+ if (
+ !importedPath ||
+ typeof importedPath !== 'string' ||
+ !importedPath.startsWith('.')
+ ) {
+ return;
+ }
+
+ // Resolve the absolute path of the imported module
+ const absoluteImportPath = path.resolve(
+ path.dirname(currentFilePath),
+ importedPath,
+ );
+
+ // Find the package information for the imported file
+ const importedPackage = findPackageName(
+ absoluteImportPath,
+ allPackagesRoot,
+ );
+
+ // If the imported file isn't in a recognized package, report issue
+ if (!importedPackage) {
+ context.report({
+ node: node.source,
+ messageId: 'relativeImportIsInvalidPackage',
+ data: { importedPath: importedPath },
+ });
+ return;
+ }
+
+ // The core check: Are the source and target packages different?
+ if (currentPackage !== importedPackage) {
+ // We found a relative import crossing package boundaries
+ context.report({
+ node: node.source, // Report the error on the source string literal
+ messageId: 'noRelativePathsForCrossPackageImport',
+ data: {
+ importedPath,
+ importedPackage,
+ importingPackage,
+ },
+ fix(fixer) {
+ return fixer.replaceText(node.source, `'${importedPackage}'`);
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 00000000..29aa23dc
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,236 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import eslint from '@eslint/js';
+import tseslint from 'typescript-eslint';
+import reactPlugin from 'eslint-plugin-react';
+import reactHooks from 'eslint-plugin-react-hooks';
+import prettierConfig from 'eslint-config-prettier';
+import importPlugin from 'eslint-plugin-import';
+import globals from 'globals';
+import licenseHeader from 'eslint-plugin-license-header';
+import noRelativeCrossPackageImports from './eslint-rules/no-relative-cross-package-imports.js';
+import path from 'node:path'; // Use node: prefix for built-ins
+import url from 'node:url';
+
+// --- ESM way to get __dirname ---
+const __filename = url.fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+// --- ---
+
+// Determine the monorepo root (assuming eslint.config.js is at the root)
+const projectRoot = __dirname;
+
+export default tseslint.config(
+ {
+ // Global ignores
+ ignores: [
+ 'node_modules/*',
+ 'eslint.config.js',
+ 'packages/cli/dist/**',
+ 'packages/core/dist/**',
+ 'packages/server/dist/**',
+ 'packages/vscode-ide-companion/dist/**',
+ 'eslint-rules/*',
+ 'bundle/**',
+ ],
+ },
+ eslint.configs.recommended,
+ ...tseslint.configs.recommended,
+ reactHooks.configs['recommended-latest'],
+ reactPlugin.configs.flat.recommended,
+ reactPlugin.configs.flat['jsx-runtime'], // Add this if you are using React 17+
+ {
+ // Settings for eslint-plugin-react
+ settings: {
+ react: {
+ version: 'detect',
+ },
+ },
+ },
+ {
+ // Import specific config
+ files: ['packages/cli/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
+ plugins: {
+ import: importPlugin,
+ },
+ settings: {
+ 'import/resolver': {
+ node: true,
+ },
+ },
+ rules: {
+ ...importPlugin.configs.recommended.rules,
+ ...importPlugin.configs.typescript.rules,
+ 'import/no-default-export': 'warn',
+ 'import/no-unresolved': 'off', // Disable for now, can be noisy with monorepos/paths
+ },
+ },
+ {
+ // General overrides and rules for the project (TS/TSX files)
+ files: ['packages/*/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ ...globals.es2021,
+ },
+ },
+ rules: {
+ // General Best Practice Rules (subset adapted for flat config)
+ '@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
+ 'arrow-body-style': ['error', 'as-needed'],
+ curly: ['error', 'multi-line'],
+ eqeqeq: ['error', 'always', { null: 'ignore' }],
+ '@typescript-eslint/consistent-type-assertions': [
+ 'error',
+ { assertionStyle: 'as' },
+ ],
+ '@typescript-eslint/explicit-member-accessibility': [
+ 'error',
+ { accessibility: 'no-public' },
+ ],
+ '@typescript-eslint/no-explicit-any': 'error',
+ '@typescript-eslint/no-inferrable-types': [
+ 'error',
+ { ignoreParameters: true, ignoreProperties: true },
+ ],
+ '@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }],
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ caughtErrorsIgnorePattern: '^_',
+ },
+ ],
+ 'no-cond-assign': 'error',
+ 'no-debugger': 'error',
+ 'no-duplicate-case': 'error',
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector: 'CallExpression[callee.name="require"]',
+ message: 'Avoid using require(). Use ES6 imports instead.',
+ },
+ {
+ selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
+ message:
+ 'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
+ },
+ ],
+ 'no-unsafe-finally': 'error',
+ 'no-unused-expressions': 'off', // Disable base rule
+ '@typescript-eslint/no-unused-expressions': [
+ // Enable TS version
+ 'error',
+ { allowShortCircuit: true, allowTernary: true },
+ ],
+ 'no-var': 'error',
+ 'object-shorthand': 'error',
+ 'one-var': ['error', 'never'],
+ 'prefer-arrow-callback': 'error',
+ 'prefer-const': ['error', { destructuring: 'all' }],
+ radix: 'error',
+ 'default-case': 'error',
+ },
+ },
+ {
+ files: ['./**/*.{tsx,ts,js}'],
+ plugins: {
+ 'license-header': licenseHeader,
+ },
+ rules: {
+ 'license-header/header': [
+ 'error',
+ [
+ '/**',
+ ' * @license',
+ ' * Copyright 2025 Google LLC',
+ ' * SPDX-License-Identifier: Apache-2.0',
+ ' */',
+ ],
+ ],
+ },
+ },
+ // extra settings for scripts that we run directly with node
+ {
+ files: ['./scripts/**/*.js', 'esbuild.config.js'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ process: 'readonly',
+ console: 'readonly',
+ },
+ },
+ rules: {
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ caughtErrorsIgnorePattern: '^_',
+ },
+ ],
+ },
+ },
+ {
+ files: ['packages/vscode-ide-companion/esbuild.js'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ process: 'readonly',
+ console: 'readonly',
+ },
+ },
+ rules: {
+ 'no-restricted-syntax': 'off',
+ '@typescript-eslint/no-require-imports': 'off',
+ },
+ },
+ // Prettier config must be last
+ prettierConfig,
+ // extra settings for scripts that we run directly with node
+ {
+ files: ['./integration-tests/**/*.js'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ process: 'readonly',
+ console: 'readonly',
+ },
+ },
+ rules: {
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ caughtErrorsIgnorePattern: '^_',
+ },
+ ],
+ },
+ },
+ // Custom eslint rules for this repo
+ {
+ files: ['packages/**/*.{js,jsx,ts,tsx}'],
+ plugins: {
+ custom: {
+ rules: {
+ 'no-relative-cross-package-imports': noRelativeCrossPackageImports,
+ },
+ },
+ },
+ rules: {
+ // Enable and configure your custom rule
+ 'custom/no-relative-cross-package-imports': [
+ 'error',
+ {
+ root: path.join(projectRoot, 'packages'),
+ },
+ ],
+ },
+ },
+);
diff --git a/integration-tests/file-system.test.js b/integration-tests/file-system.test.js
new file mode 100644
index 00000000..87e9efe2
--- /dev/null
+++ b/integration-tests/file-system.test.js
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { strict as assert } from 'assert';
+import { test } from 'node:test';
+import { TestRig } from './test-helper.js';
+
+test('reads a file', (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+ rig.createFile('test.txt', 'hello world');
+
+ const output = rig.run(`read the file name test.txt`);
+
+ assert.ok(output.toLowerCase().includes('hello'));
+});
+
+test('writes a file', (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+ rig.createFile('test.txt', '');
+
+ rig.run(`edit test.txt to have a hello world message`);
+
+ const fileContent = rig.readFile('test.txt');
+ assert.ok(fileContent.toLowerCase().includes('hello'));
+});
diff --git a/integration-tests/google_web_search.test.js b/integration-tests/google_web_search.test.js
new file mode 100644
index 00000000..a8968117
--- /dev/null
+++ b/integration-tests/google_web_search.test.js
@@ -0,0 +1,19 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { test } from 'node:test';
+import { strict as assert } from 'assert';
+import { TestRig } from './test-helper.js';
+
+test('should be able to search the web', async (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+
+ const prompt = `what planet do we live on`;
+ const result = await rig.run(prompt);
+
+ assert.ok(result.toLowerCase().includes('earth'));
+});
diff --git a/integration-tests/list_directory.test.js b/integration-tests/list_directory.test.js
new file mode 100644
index 00000000..af7aae78
--- /dev/null
+++ b/integration-tests/list_directory.test.js
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { test } from 'node:test';
+import { strict as assert } from 'assert';
+import { TestRig } from './test-helper.js';
+
+test('should be able to list a directory', async (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+ rig.createFile('file1.txt', 'file 1 content');
+ rig.mkdir('subdir');
+ rig.sync();
+
+ const prompt = `Can you list the files in the current directory. Display them in the style of 'ls'`;
+ const result = rig.run(prompt);
+
+ const lines = result.split('\n').filter((line) => line.trim() !== '');
+ assert.ok(lines.some((line) => line.includes('file1.txt')));
+ assert.ok(lines.some((line) => line.includes('subdir')));
+});
diff --git a/integration-tests/read_many_files.test.js b/integration-tests/read_many_files.test.js
new file mode 100644
index 00000000..7e770036
--- /dev/null
+++ b/integration-tests/read_many_files.test.js
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { test } from 'node:test';
+import { strict as assert } from 'assert';
+import { TestRig } from './test-helper.js';
+
+test.skip('should be able to read multiple files', async (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+ rig.createFile('file1.txt', 'file 1 content');
+ rig.createFile('file2.txt', 'file 2 content');
+
+ const prompt = `Read the files in this directory, list them and print them to the screen`;
+ const result = await rig.run(prompt);
+
+ assert.ok(result.includes('file 1 content'));
+ assert.ok(result.includes('file 2 content'));
+});
diff --git a/integration-tests/replace.test.js b/integration-tests/replace.test.js
new file mode 100644
index 00000000..060aba55
--- /dev/null
+++ b/integration-tests/replace.test.js
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { test } from 'node:test';
+import { strict as assert } from 'assert';
+import { TestRig } from './test-helper.js';
+
+test('should be able to replace content in a file', async (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+
+ const fileName = 'file_to_replace.txt';
+ rig.createFile(fileName, 'original content');
+ const prompt = `Can you replace 'original' with 'replaced' in the file 'file_to_replace.txt'`;
+
+ await rig.run(prompt);
+ const newFileContent = rig.readFile(fileName);
+ assert.strictEqual(newFileContent, 'replaced content');
+});
diff --git a/integration-tests/run-tests.js b/integration-tests/run-tests.js
new file mode 100644
index 00000000..5923dfcf
--- /dev/null
+++ b/integration-tests/run-tests.js
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { spawnSync, spawn } from 'child_process';
+import { mkdirSync, rmSync, createWriteStream } from 'fs';
+import { join, dirname, basename } from 'path';
+import { fileURLToPath } from 'url';
+import { glob } from 'glob';
+
+async function main() {
+ const __dirname = dirname(fileURLToPath(import.meta.url));
+ const rootDir = join(__dirname, '..');
+ const integrationTestsDir = join(rootDir, '.integration-tests');
+
+ if (process.env.GEMINI_SANDBOX === 'docker' && !process.env.IS_DOCKER) {
+ console.log('Building sandbox for Docker...');
+ const buildResult = spawnSync('npm', ['run', 'build:all'], {
+ stdio: 'inherit',
+ });
+ if (buildResult.status !== 0) {
+ console.error('Sandbox build failed.');
+ process.exit(1);
+ }
+ }
+
+ const runId = `${Date.now()}`;
+ const runDir = join(integrationTestsDir, runId);
+
+ mkdirSync(runDir, { recursive: true });
+
+ const args = process.argv.slice(2);
+ const keepOutput =
+ process.env.KEEP_OUTPUT === 'true' || args.includes('--keep-output');
+ if (keepOutput) {
+ const keepOutputIndex = args.indexOf('--keep-output');
+ if (keepOutputIndex > -1) {
+ args.splice(keepOutputIndex, 1);
+ }
+ console.log(`Keeping output for test run in: ${runDir}`);
+ }
+
+ const verbose = args.includes('--verbose');
+ if (verbose) {
+ const verboseIndex = args.indexOf('--verbose');
+ if (verboseIndex > -1) {
+ args.splice(verboseIndex, 1);
+ }
+ }
+
+ const testPatterns =
+ args.length > 0
+ ? args.map((arg) => `integration-tests/${arg}.test.js`)
+ : ['integration-tests/*.test.js'];
+ const testFiles = glob.sync(testPatterns, { cwd: rootDir, absolute: true });
+
+ for (const testFile of testFiles) {
+ const testFileName = basename(testFile);
+ console.log(`\tFound test file: ${testFileName}`);
+ }
+
+ let allTestsPassed = true;
+
+ for (const testFile of testFiles) {
+ const testFileName = basename(testFile);
+ const testFileDir = join(runDir, testFileName);
+ mkdirSync(testFileDir, { recursive: true });
+
+ console.log(
+ `------------- Running test file: ${testFileName} ------------------------------`,
+ );
+
+ const nodeArgs = ['--test'];
+ if (verbose) {
+ nodeArgs.push('--test-reporter=spec');
+ }
+ nodeArgs.push(testFile);
+
+ const child = spawn('node', nodeArgs, {
+ stdio: 'pipe',
+ env: {
+ ...process.env,
+ GEMINI_CLI_INTEGRATION_TEST: 'true',
+ INTEGRATION_TEST_FILE_DIR: testFileDir,
+ KEEP_OUTPUT: keepOutput.toString(),
+ VERBOSE: verbose.toString(),
+ TEST_FILE_NAME: testFileName,
+ },
+ });
+
+ let outputStream;
+ if (keepOutput) {
+ const outputFile = join(testFileDir, 'output.log');
+ outputStream = createWriteStream(outputFile);
+ console.log(`Output for ${testFileName} written to: ${outputFile}`);
+ }
+
+ child.stdout.on('data', (data) => {
+ if (verbose) {
+ process.stdout.write(data);
+ }
+ if (outputStream) {
+ outputStream.write(data);
+ }
+ });
+
+ child.stderr.on('data', (data) => {
+ if (verbose) {
+ process.stderr.write(data);
+ }
+ if (outputStream) {
+ outputStream.write(data);
+ }
+ });
+
+ const exitCode = await new Promise((resolve) => {
+ child.on('close', (code) => {
+ if (outputStream) {
+ outputStream.end(() => {
+ resolve(code);
+ });
+ } else {
+ resolve(code);
+ }
+ });
+ });
+
+ if (exitCode !== 0) {
+ console.error(`Test file failed: ${testFileName}`);
+ allTestsPassed = false;
+ }
+ }
+
+ if (!keepOutput) {
+ rmSync(runDir, { recursive: true, force: true });
+ }
+
+ if (!allTestsPassed) {
+ console.error('One or more test files failed.');
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/integration-tests/run_shell_command.test.js b/integration-tests/run_shell_command.test.js
new file mode 100644
index 00000000..52aee194
--- /dev/null
+++ b/integration-tests/run_shell_command.test.js
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { test } from 'node:test';
+import { strict as assert } from 'assert';
+import { TestRig } from './test-helper.js';
+
+test('should be able to run a shell command', async (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+ rig.createFile('blah.txt', 'some content');
+
+ const prompt = `Can you use ls to list the contexts of the current folder`;
+ const result = rig.run(prompt);
+
+ assert.ok(result.includes('blah.txt'));
+});
+
+test('should be able to run a shell command via stdin', async (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+ rig.createFile('blah.txt', 'some content');
+
+ const prompt = `Can you use ls to list the contexts of the current folder`;
+ const result = rig.run({ stdin: prompt });
+
+ assert.ok(result.includes('blah.txt'));
+});
diff --git a/integration-tests/save_memory.test.js b/integration-tests/save_memory.test.js
new file mode 100644
index 00000000..0716f978
--- /dev/null
+++ b/integration-tests/save_memory.test.js
@@ -0,0 +1,21 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { test } from 'node:test';
+import { strict as assert } from 'assert';
+import { TestRig } from './test-helper.js';
+
+test('should be able to save to memory', async (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+
+ const prompt = `remember that my favorite color is blue.
+
+ what is my favorite color? tell me that and surround it with $ symbol`;
+ const result = await rig.run(prompt);
+
+ assert.ok(result.toLowerCase().includes('$blue$'));
+});
diff --git a/integration-tests/simple-mcp-server.test.js b/integration-tests/simple-mcp-server.test.js
new file mode 100644
index 00000000..d585609e
--- /dev/null
+++ b/integration-tests/simple-mcp-server.test.js
@@ -0,0 +1,70 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { test, describe, before, after } from 'node:test';
+import { strict as assert } from 'node:assert';
+import { TestRig } from './test-helper.js';
+import { spawn } from 'child_process';
+import { join } from 'path';
+import { fileURLToPath } from 'url';
+import { writeFileSync, unlinkSync } from 'fs';
+
+const __dirname = fileURLToPath(new URL('.', import.meta.url));
+const serverScriptPath = join(__dirname, './temp-server.js');
+
+const serverScript = `
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+import { z } from 'zod';
+
+const server = new McpServer({
+ name: 'addition-server',
+ version: '1.0.0',
+});
+
+server.registerTool(
+ 'add',
+ {
+ title: 'Addition Tool',
+ description: 'Add two numbers',
+ inputSchema: { a: z.number(), b: z.number() },
+ },
+ async ({ a, b }) => ({
+ content: [{ type: 'text', text: String(a + b) }],
+ }),
+);
+
+const transport = new StdioServerTransport();
+await server.connect(transport);
+`;
+
+describe('simple-mcp-server', () => {
+ const rig = new TestRig();
+ let child;
+
+ before(() => {
+ writeFileSync(serverScriptPath, serverScript);
+ child = spawn('node', [serverScriptPath], {
+ stdio: ['pipe', 'pipe', 'pipe'],
+ });
+ child.stderr.on('data', (data) => {
+ console.error(`stderr: ${data}`);
+ });
+ // Wait for the server to be ready
+ return new Promise((resolve) => setTimeout(resolve, 500));
+ });
+
+ after(() => {
+ child.kill();
+ unlinkSync(serverScriptPath);
+ });
+
+ test('should add two numbers', () => {
+ rig.setup('should add two numbers');
+ const output = rig.run('add 5 and 10');
+ assert.ok(output.includes('15'));
+ });
+});
diff --git a/integration-tests/test-helper.js b/integration-tests/test-helper.js
new file mode 100644
index 00000000..7ee3db87
--- /dev/null
+++ b/integration-tests/test-helper.js
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { execSync } from 'child_process';
+import { mkdirSync, writeFileSync, readFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { env } from 'process';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+function sanitizeTestName(name) {
+ return name
+ .toLowerCase()
+ .replace(/[^a-z0-9]/g, '-')
+ .replace(/-+/g, '-');
+}
+
+export class TestRig {
+ constructor() {
+ this.bundlePath = join(__dirname, '..', 'bundle/gemini.js');
+ this.testDir = null;
+ }
+
+ setup(testName) {
+ this.testName = testName;
+ const sanitizedName = sanitizeTestName(testName);
+ this.testDir = join(env.INTEGRATION_TEST_FILE_DIR, sanitizedName);
+ mkdirSync(this.testDir, { recursive: true });
+ }
+
+ createFile(fileName, content) {
+ const filePath = join(this.testDir, fileName);
+ writeFileSync(filePath, content);
+ return filePath;
+ }
+
+ mkdir(dir) {
+ mkdirSync(join(this.testDir, dir));
+ }
+
+ sync() {
+ // ensure file system is done before spawning
+ execSync('sync', { cwd: this.testDir });
+ }
+
+ run(promptOrOptions, ...args) {
+ let command = `node ${this.bundlePath} --yolo`;
+ const execOptions = {
+ cwd: this.testDir,
+ encoding: 'utf-8',
+ };
+
+ if (typeof promptOrOptions === 'string') {
+ command += ` --prompt "${promptOrOptions}"`;
+ } else if (
+ typeof promptOrOptions === 'object' &&
+ promptOrOptions !== null
+ ) {
+ if (promptOrOptions.prompt) {
+ command += ` --prompt "${promptOrOptions.prompt}"`;
+ }
+ if (promptOrOptions.stdin) {
+ execOptions.input = promptOrOptions.stdin;
+ }
+ }
+
+ command += ` ${args.join(' ')}`;
+
+ const output = execSync(command, execOptions);
+
+ if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') {
+ const testId = `${env.TEST_FILE_NAME.replace(
+ '.test.js',
+ '',
+ )}:${this.testName.replace(/ /g, '-')}`;
+ console.log(`--- TEST: ${testId} ---`);
+ console.log(output);
+ console.log(`--- END TEST: ${testId} ---`);
+ }
+
+ return output;
+ }
+
+ readFile(fileName) {
+ const content = readFileSync(join(this.testDir, fileName), 'utf-8');
+ if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') {
+ const testId = `${env.TEST_FILE_NAME.replace(
+ '.test.js',
+ '',
+ )}:${this.testName.replace(/ /g, '-')}`;
+ console.log(`--- FILE: ${testId}/${fileName} ---`);
+ console.log(content);
+ console.log(`--- END FILE: ${testId}/${fileName} ---`);
+ }
+ return content;
+ }
+}
diff --git a/integration-tests/write_file.test.js b/integration-tests/write_file.test.js
new file mode 100644
index 00000000..46a15f3c
--- /dev/null
+++ b/integration-tests/write_file.test.js
@@ -0,0 +1,21 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { test } from 'node:test';
+import { strict as assert } from 'assert';
+import { TestRig } from './test-helper.js';
+
+test('should be able to write a file', async (t) => {
+ const rig = new TestRig();
+ rig.setup(t.name);
+ const prompt = `show me an example of using the write tool. put a dad joke in dad.txt`;
+
+ await rig.run(prompt);
+ const newFilePath = 'dad.txt';
+
+ const newFileContent = rig.readFile(newFilePath);
+ assert.notEqual(newFileContent, '');
+});
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..7079f565
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,12214 @@
+{
+ "name": "@qwen/qwen-code",
+ "version": "0.0.1-dev1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@qwen/qwen-code",
+ "version": "0.0.1-dev1",
+ "workspaces": [
+ "packages/*"
+ ],
+ "bin": {
+ "qwen": "bundle/gemini.js"
+ },
+ "devDependencies": {
+ "@types/micromatch": "^4.0.9",
+ "@types/mime-types": "^3.0.1",
+ "@types/minimatch": "^5.1.2",
+ "@types/semver": "^7.7.0",
+ "@types/shell-quote": "^1.7.5",
+ "@vitest/coverage-v8": "^3.1.1",
+ "concurrently": "^9.2.0",
+ "cross-env": "^7.0.3",
+ "esbuild": "^0.25.0",
+ "eslint": "^9.24.0",
+ "eslint-config-prettier": "^10.1.2",
+ "eslint-plugin-import": "^2.31.0",
+ "eslint-plugin-license-header": "^0.8.0",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "glob": "^10.4.5",
+ "globals": "^16.0.0",
+ "json": "^11.0.0",
+ "lodash": "^4.17.21",
+ "memfs": "^4.17.2",
+ "prettier": "^3.5.3",
+ "react-devtools-core": "^4.28.5",
+ "typescript-eslint": "^8.30.1",
+ "vitest": "^3.2.4",
+ "yargs": "^18.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@alcalzone/ansi-tokenize": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz",
+ "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14.13.1"
+ }
+ },
+ "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@qwen/qwen-code": {
+ "resolved": "packages/cli",
+ "link": true
+ },
+ "node_modules/@qwen/qwen-code-core": {
+ "resolved": "packages/core",
+ "link": true
+ },
+ "node_modules/@qwen/qwen-code-vscode-ide-companion": {
+ "resolved": "packages/vscode-ide-companion",
+ "link": true
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
+ "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
+ "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
+ "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
+ "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
+ "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.0.2",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
+ "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",
+ "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",
+ "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",
+ "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",
+ "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",
+ "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",
+ "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",
+ "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",
+ "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",
+ "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",
+ "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",
+ "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",
+ "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",
+ "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",
+ "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",
+ "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",
+ "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",
+ "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",
+ "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",
+ "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",
+ "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz",
+ "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",
+ "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",
+ "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",
+ "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",
+ "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.20.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz",
+ "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz",
+ "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
+ "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.29.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
+ "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz",
+ "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
+ "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@google/genai": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.8.0.tgz",
+ "integrity": "sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "google-auth-library": "^9.14.2",
+ "ws": "^8.18.0",
+ "zod": "^3.22.4",
+ "zod-to-json-schema": "^3.22.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/sdk": "^1.11.0"
+ },
+ "peerDependenciesMeta": {
+ "@modelcontextprotocol/sdk": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
+ "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/proto-loader": "^0.7.13",
+ "@js-sdsl/ordered-map": "^4.4.2"
+ },
+ "engines": {
+ "node": ">=12.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.7.15",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
+ "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.2.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/@grpc/proto-loader/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz",
+ "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@js-sdsl/ordered-map": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+ "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/js-sdsl"
+ }
+ },
+ "node_modules/@jsonjoy.com/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/json-pack": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz",
+ "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/base64": "^1.1.1",
+ "@jsonjoy.com/util": "^1.1.2",
+ "hyperdyperid": "^1.2.0",
+ "thingies": "^1.20.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/util": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz",
+ "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@kwsites/file-exists": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+ "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1"
+ }
+ },
+ "node_modules/@kwsites/promise-deferred": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
+ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
+ "license": "MIT"
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz",
+ "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.6",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz",
+ "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/context-async-hooks": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz",
+ "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
+ "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-logs-otlp-grpc": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.52.1.tgz",
+ "integrity": "sha512-sXgcp4fsL3zCo96A0LmFIGYOj2LSEDI6wD7nBYRhuDDxeRsk18NQgqRVlCf4VIyTBZzGu1M7yOtdFukQPgII1A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/grpc-js": "^1.7.1",
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/otlp-grpc-exporter-base": "0.52.1",
+ "@opentelemetry/otlp-transformer": "0.52.1",
+ "@opentelemetry/sdk-logs": "0.52.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.52.1.tgz",
+ "integrity": "sha512-CE0f1IEE1GQj8JWl/BxKvKwx9wBTLR09OpPQHaIs5LGBw3ODu8ek5kcbrHPNsFYh/pWh+pcjbZQoxq3CqvQVnA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/grpc-js": "^1.7.1",
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/exporter-metrics-otlp-http": "0.52.1",
+ "@opentelemetry/otlp-exporter-base": "0.52.1",
+ "@opentelemetry/otlp-grpc-exporter-base": "0.52.1",
+ "@opentelemetry/otlp-transformer": "0.52.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/sdk-metrics": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-metrics-otlp-http": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.52.1.tgz",
+ "integrity": "sha512-oAHPOy1sZi58bwqXaucd19F/v7+qE2EuVslQOEeLQT94CDuZJJ4tbWzx8DpYBTrOSzKqqrMtx9+PMxkrcbxOyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/otlp-exporter-base": "0.52.1",
+ "@opentelemetry/otlp-transformer": "0.52.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/sdk-metrics": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-trace-otlp-grpc": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz",
+ "integrity": "sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/grpc-js": "^1.7.1",
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/otlp-grpc-exporter-base": "0.52.1",
+ "@opentelemetry/otlp-transformer": "0.52.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/sdk-trace-base": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-trace-otlp-http": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.52.1.tgz",
+ "integrity": "sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/otlp-exporter-base": "0.52.1",
+ "@opentelemetry/otlp-transformer": "0.52.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/sdk-trace-base": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-trace-otlp-proto": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.52.1.tgz",
+ "integrity": "sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/otlp-exporter-base": "0.52.1",
+ "@opentelemetry/otlp-transformer": "0.52.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/sdk-trace-base": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-zipkin": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.1.tgz",
+ "integrity": "sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/sdk-trace-base": "1.25.1",
+ "@opentelemetry/semantic-conventions": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz",
+ "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.52.1",
+ "@types/shimmer": "^1.0.2",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1",
+ "semver": "^7.5.2",
+ "shimmer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.1.tgz",
+ "integrity": "sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/instrumentation": "0.52.1",
+ "@opentelemetry/semantic-conventions": "1.25.1",
+ "semver": "^7.5.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-exporter-base": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.52.1.tgz",
+ "integrity": "sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/otlp-transformer": "0.52.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-grpc-exporter-base": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz",
+ "integrity": "sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/grpc-js": "^1.7.1",
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/otlp-exporter-base": "0.52.1",
+ "@opentelemetry/otlp-transformer": "0.52.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.52.1.tgz",
+ "integrity": "sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.52.1",
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/sdk-logs": "0.52.1",
+ "@opentelemetry/sdk-metrics": "1.25.1",
+ "@opentelemetry/sdk-trace-base": "1.25.1",
+ "protobufjs": "^7.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/propagator-b3": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.25.1.tgz",
+ "integrity": "sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/propagator-jaeger": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.25.1.tgz",
+ "integrity": "sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz",
+ "integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/semantic-conventions": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.52.1.tgz",
+ "integrity": "sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.52.1",
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/resources": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.4.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
+ "integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/resources": "1.25.1",
+ "lodash.merge": "^4.6.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-node": {
+ "version": "0.52.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz",
+ "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.52.1",
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1",
+ "@opentelemetry/exporter-trace-otlp-http": "0.52.1",
+ "@opentelemetry/exporter-trace-otlp-proto": "0.52.1",
+ "@opentelemetry/exporter-zipkin": "1.25.1",
+ "@opentelemetry/instrumentation": "0.52.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/sdk-logs": "0.52.1",
+ "@opentelemetry/sdk-metrics": "1.25.1",
+ "@opentelemetry/sdk-trace-base": "1.25.1",
+ "@opentelemetry/sdk-trace-node": "1.25.1",
+ "@opentelemetry/semantic-conventions": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
+ "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/resources": "1.25.1",
+ "@opentelemetry/semantic-conventions": "1.25.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-node": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.25.1.tgz",
+ "integrity": "sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/context-async-hooks": "1.25.1",
+ "@opentelemetry/core": "1.25.1",
+ "@opentelemetry/propagator-b3": "1.25.1",
+ "@opentelemetry/propagator-jaeger": "1.25.1",
+ "@opentelemetry/sdk-trace-base": "1.25.1",
+ "semver": "^7.5.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-node/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.25.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz",
+ "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@pnpm/config.env-replace": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
+ "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.22.0"
+ }
+ },
+ "node_modules/@pnpm/network.ca-file": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz",
+ "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "4.2.10"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ }
+ },
+ "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": {
+ "version": "4.2.10",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
+ "license": "ISC"
+ },
+ "node_modules/@pnpm/npm-conf": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz",
+ "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@pnpm/config.env-replace": "^1.1.0",
+ "@pnpm/network.ca-file": "^1.0.1",
+ "config-chain": "^1.1.11"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz",
+ "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz",
+ "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz",
+ "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz",
+ "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz",
+ "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz",
+ "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz",
+ "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz",
+ "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz",
+ "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz",
+ "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz",
+ "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz",
+ "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz",
+ "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz",
+ "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz",
+ "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz",
+ "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz",
+ "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz",
+ "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz",
+ "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz",
+ "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@selderee/plugin-htmlparser2": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
+ "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "^5.0.3",
+ "selderee": "^0.11.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.34.37",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
+ "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/braces": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz",
+ "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
+ "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*"
+ }
+ },
+ "node_modules/@types/command-exists": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@types/command-exists/-/command-exists-1.2.3.tgz",
+ "integrity": "sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/configstore": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-6.0.2.tgz",
+ "integrity": "sha512-OS//b51j9uyR3zvwD04Kfs5kHpve2qalQ18JhY/ho3voGYUTPLEG90/ocfKPI48hyHH8T04f7KEEbK6Ue60oZQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/diff": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz",
+ "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/dotenv": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz",
+ "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
+ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz",
+ "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/minimatch": "^5.1.2",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/gradient-string": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@types/gradient-string/-/gradient-string-1.1.6.tgz",
+ "integrity": "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/tinycolor2": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/html-to-text": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz",
+ "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/micromatch": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz",
+ "integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/braces": "*"
+ }
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz",
+ "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/normalize-package-data": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.1.8",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
+ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.1.6",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
+ "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.7.0.tgz",
+ "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.5",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
+ "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.8",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
+ "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/shell-quote": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz",
+ "integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/shimmer": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
+ "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/tinycolor2": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz",
+ "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/update-notifier": {
+ "version": "6.0.8",
+ "resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-6.0.8.tgz",
+ "integrity": "sha512-IlDFnfSVfYQD+cKIg63DEXn3RFmd7W1iYtKQsJodcHK9R1yr8aKbKaPKfBxzPpcHCq2DU8zUq4PIPmy19Thjfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/configstore": "*",
+ "boxen": "^7.1.1"
+ }
+ },
+ "node_modules/@types/vscode": {
+ "version": "1.102.0",
+ "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.102.0.tgz",
+ "integrity": "sha512-V9sFXmcXz03FtYTSUsYsu5K0Q9wH9w9V25slddcxrh5JgORD14LpnOA7ov0L9ALi+6HrTjskLJ/tY5zeRF3TFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.33",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
+ "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.3",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
+ "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.35.0",
+ "@typescript-eslint/type-utils": "8.35.0",
+ "@typescript-eslint/utils": "8.35.0",
+ "@typescript-eslint/visitor-keys": "8.35.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.35.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz",
+ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.35.0",
+ "@typescript-eslint/types": "8.35.0",
+ "@typescript-eslint/typescript-estree": "8.35.0",
+ "@typescript-eslint/visitor-keys": "8.35.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
+ "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.35.0",
+ "@typescript-eslint/types": "^8.35.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
+ "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.35.0",
+ "@typescript-eslint/visitor-keys": "8.35.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
+ "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
+ "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.35.0",
+ "@typescript-eslint/utils": "8.35.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
+ "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
+ "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.35.0",
+ "@typescript-eslint/tsconfig-utils": "8.35.0",
+ "@typescript-eslint/types": "8.35.0",
+ "@typescript-eslint/visitor-keys": "8.35.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
+ "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.35.0",
+ "@typescript-eslint/types": "8.35.0",
+ "@typescript-eslint/typescript-estree": "8.35.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
+ "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.35.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
+ "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@bcoe/v8-coverage": "^1.0.2",
+ "ast-v8-to-istanbul": "^0.3.3",
+ "debug": "^4.4.1",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.1.7",
+ "magic-string": "^0.30.17",
+ "magicast": "^0.3.5",
+ "std-env": "^3.9.0",
+ "test-exclude": "^7.0.1",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "3.2.4",
+ "vitest": "3.2.4"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-align": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+ "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.1.0"
+ }
+ },
+ "node_modules/ansi-align/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-align/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-align/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-align/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
+ "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+ "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz",
+ "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^9.0.1"
+ }
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/atomically": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz",
+ "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==",
+ "dependencies": {
+ "stubborn-fs": "^1.2.5",
+ "when-exit": "^2.1.1"
+ }
+ },
+ "node_modules/auto-bind": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
+ "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "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"
+ },
+ "node_modules/bignumber.js": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
+ "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/boxen": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz",
+ "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-align": "^3.0.1",
+ "camelcase": "^7.0.1",
+ "chalk": "^5.2.0",
+ "cli-boxes": "^3.0.0",
+ "string-width": "^5.1.2",
+ "type-fest": "^2.13.0",
+ "widest-line": "^4.0.1",
+ "wrap-ansi": "^8.1.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/boxen/node_modules/chalk": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
+ "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cfonts": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/cfonts/-/cfonts-3.3.0.tgz",
+ "integrity": "sha512-RlVxeEw2FXWI5Bs9LD0/Ef3bsQIc9m6lK/DINN20HIW0Y0YHUO2jjy88cot9YKZITiRTCdWzTfLmTyx47HeSLA==",
+ "license": "GPL-3.0-or-later",
+ "dependencies": {
+ "supports-color": "^8",
+ "window-size": "^1"
+ },
+ "bin": {
+ "cfonts": "bin/index.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
+ "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "license": "MIT"
+ },
+ "node_modules/cli-boxes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
+ "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+ "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
+ "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "license": "MIT"
+ },
+ "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/code-excerpt": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
+ "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
+ "license": "MIT",
+ "dependencies": {
+ "convert-to-spaces": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/command-exists": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
+ "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==",
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concurrently": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
+ "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "lodash": "^4.17.21",
+ "rxjs": "^7.8.1",
+ "shell-quote": "^1.8.1",
+ "supports-color": "^8.1.1",
+ "tree-kill": "^1.2.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/concurrently/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/concurrently/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/concurrently/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concurrently/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/concurrently/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/concurrently/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/concurrently/node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/concurrently/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/config-chain": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+ "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "node_modules/config-chain/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/configstore": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.0.0.tgz",
+ "integrity": "sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "atomically": "^2.0.3",
+ "dot-prop": "^9.0.0",
+ "graceful-fs": "^4.2.11",
+ "xdg-basedir": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/yeoman/configstore?sponsor=1"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-to-spaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
+ "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
+ "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/default-browser": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
+ "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
+ "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/diff": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+ "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dot-prop": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz",
+ "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^4.18.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dot-prop/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.1.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.1.0.tgz",
+ "integrity": "sha512-tG9VUTJTuju6GcXgbdsOuRhupE8cb4mRgY5JLRCh4MtGoVo3/gfGUtOMwmProM6d0ba2mCFvv+WrpYJV6qgJXQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
+ "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
+ "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.6",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.4",
+ "safe-array-concat": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-toolkit": {
+ "version": "1.39.5",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.5.tgz",
+ "integrity": "sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
+ "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.6",
+ "@esbuild/android-arm": "0.25.6",
+ "@esbuild/android-arm64": "0.25.6",
+ "@esbuild/android-x64": "0.25.6",
+ "@esbuild/darwin-arm64": "0.25.6",
+ "@esbuild/darwin-x64": "0.25.6",
+ "@esbuild/freebsd-arm64": "0.25.6",
+ "@esbuild/freebsd-x64": "0.25.6",
+ "@esbuild/linux-arm": "0.25.6",
+ "@esbuild/linux-arm64": "0.25.6",
+ "@esbuild/linux-ia32": "0.25.6",
+ "@esbuild/linux-loong64": "0.25.6",
+ "@esbuild/linux-mips64el": "0.25.6",
+ "@esbuild/linux-ppc64": "0.25.6",
+ "@esbuild/linux-riscv64": "0.25.6",
+ "@esbuild/linux-s390x": "0.25.6",
+ "@esbuild/linux-x64": "0.25.6",
+ "@esbuild/netbsd-arm64": "0.25.6",
+ "@esbuild/netbsd-x64": "0.25.6",
+ "@esbuild/openbsd-arm64": "0.25.6",
+ "@esbuild/openbsd-x64": "0.25.6",
+ "@esbuild/openharmony-arm64": "0.25.6",
+ "@esbuild/sunos-x64": "0.25.6",
+ "@esbuild/win32-arm64": "0.25.6",
+ "@esbuild/win32-ia32": "0.25.6",
+ "@esbuild/win32-x64": "0.25.6"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-goat": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz",
+ "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.29.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
+ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.20.1",
+ "@eslint/config-helpers": "^0.2.1",
+ "@eslint/core": "^0.14.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.29.0",
+ "@eslint/plugin-kit": "^0.3.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.5",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
+ "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
+ "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
+ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-license-header": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-license-header/-/eslint-plugin-license-header-0.8.0.tgz",
+ "integrity": "sha512-khTCz6G3JdoQfwrtY4XKl98KW4PpnWUKuFx8v+twIRhJADEyYglMDC0td8It75C1MZ88gcvMusWuUlJsos7gYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "requireindex": "^1.2.0"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz",
+ "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz",
+ "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
+ "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
+ "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
+ "node_modules/figures": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+ "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-unicode-supported": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/find-up-simple": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
+ "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gaxios": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz",
+ "integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "node-fetch": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
+ "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "gaxios": "^6.1.1",
+ "google-logging-utils": "^0.0.2",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gcp-metadata/node_modules/gaxios": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gcp-metadata/node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gcp-metadata/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/gcp-metadata/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/gcp-metadata/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/global-directory": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
+ "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ini": "4.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
+ "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/google-auth-library": {
+ "version": "9.15.1",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
+ "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "gaxios": "^6.1.1",
+ "gcp-metadata": "^6.1.0",
+ "gtoken": "^7.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/google-auth-library/node_modules/gaxios": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/google-auth-library/node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/google-auth-library/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/google-auth-library/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/google-auth-library/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/google-logging-utils": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
+ "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/gradient-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-2.0.2.tgz",
+ "integrity": "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "tinygradient": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/gtoken": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+ "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
+ "license": "MIT",
+ "dependencies": {
+ "gaxios": "^6.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/gtoken/node_modules/gaxios": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gtoken/node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gtoken/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/gtoken/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/gtoken/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
+ "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-to-text": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
+ "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@selderee/plugin-htmlparser2": "^0.11.0",
+ "deepmerge": "^4.3.1",
+ "dom-serializer": "^2.0.0",
+ "htmlparser2": "^8.0.2",
+ "selderee": "^0.11.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-errors/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/hyperdyperid": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
+ "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.18"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-in-the-middle": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz",
+ "integrity": "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-import-attributes": "^1.9.5",
+ "cjs-module-lexer": "^1.2.2",
+ "module-details-from-path": "^1.0.3"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+ "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/index-to-position": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz",
+ "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
+ "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ink": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ink/-/ink-6.0.1.tgz",
+ "integrity": "sha512-vhhFrCodTHZAPPSdMYzLEbeI0Ug37R9j6yA0kLKok9kSK53lQtj/RJhEQJUjq6OwT4N33nxqSRd/7yXhEhVPIw==",
+ "license": "MIT",
+ "dependencies": {
+ "@alcalzone/ansi-tokenize": "^0.1.3",
+ "ansi-escapes": "^7.0.0",
+ "ansi-styles": "^6.2.1",
+ "auto-bind": "^5.0.1",
+ "chalk": "^5.3.0",
+ "cli-boxes": "^3.0.0",
+ "cli-cursor": "^4.0.0",
+ "cli-truncate": "^4.0.0",
+ "code-excerpt": "^4.0.0",
+ "es-toolkit": "^1.22.0",
+ "indent-string": "^5.0.0",
+ "is-in-ci": "^1.0.0",
+ "patch-console": "^2.0.0",
+ "react-reconciler": "^0.32.0",
+ "scheduler": "^0.23.0",
+ "signal-exit": "^3.0.7",
+ "slice-ansi": "^7.1.0",
+ "stack-utils": "^2.0.6",
+ "string-width": "^7.2.0",
+ "type-fest": "^4.27.0",
+ "widest-line": "^5.0.0",
+ "wrap-ansi": "^9.0.0",
+ "ws": "^8.18.0",
+ "yoga-layout": "~3.2.1"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@types/react": ">=19.0.0",
+ "react": ">=19.0.0",
+ "react-devtools-core": "^4.19.1"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react-devtools-core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ink-big-text": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ink-big-text/-/ink-big-text-2.0.0.tgz",
+ "integrity": "sha512-Juzqv+rIOLGuhMJiE50VtS6dg6olWfzFdL7wsU/EARSL5Eaa5JNXMogMBm9AkjgzO2Y3UwWCOh87jbhSn8aNdw==",
+ "license": "MIT",
+ "dependencies": {
+ "cfonts": "^3.1.1",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ },
+ "peerDependencies": {
+ "ink": ">=4",
+ "react": ">=18"
+ }
+ },
+ "node_modules/ink-gradient": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ink-gradient/-/ink-gradient-3.0.0.tgz",
+ "integrity": "sha512-OVyPBovBxE1tFcBhSamb+P1puqDP6pG3xFe2W9NiLgwUZd9RbcjBeR7twLbliUT9navrUstEf1ZcPKKvx71BsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/gradient-string": "^1.1.2",
+ "gradient-string": "^2.0.2",
+ "prop-types": "^15.8.1",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ },
+ "peerDependencies": {
+ "ink": ">=4"
+ }
+ },
+ "node_modules/ink-link": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ink-link/-/ink-link-4.1.0.tgz",
+ "integrity": "sha512-3nNyJXum0FJIKAXBK8qat2jEOM41nJ1J60NRivwgK9Xh92R5UMN/k4vbz0A9xFzhJVrlf4BQEmmxMgXkCE1Jeg==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.1",
+ "terminal-link": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ },
+ "peerDependencies": {
+ "ink": ">=4"
+ }
+ },
+ "node_modules/ink-select-input": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ink-select-input/-/ink-select-input-6.2.0.tgz",
+ "integrity": "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "figures": "^6.1.0",
+ "to-rotated": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "ink": ">=5.0.0",
+ "react": ">=18.0.0"
+ }
+ },
+ "node_modules/ink-spinner": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ink-spinner/-/ink-spinner-5.0.0.tgz",
+ "integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==",
+ "license": "MIT",
+ "dependencies": {
+ "cli-spinners": "^2.7.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "peerDependencies": {
+ "ink": ">=4.0.0",
+ "react": ">=18.0.0"
+ }
+ },
+ "node_modules/ink-testing-library": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-4.0.0.tgz",
+ "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ink/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/ink/node_modules/chalk": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/ink/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "license": "MIT"
+ },
+ "node_modules/ink/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/ink/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ink/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ink/node_modules/widest-line": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
+ "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ink/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-accessor-descriptor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz",
+ "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "license": "MIT"
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-descriptor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz",
+ "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-descriptor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz",
+ "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.1",
+ "is-data-descriptor": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
+ "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.0",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-in-ci": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz",
+ "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==",
+ "license": "MIT",
+ "bin": {
+ "is-in-ci": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-installed-globally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz",
+ "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "global-directory": "^4.0.1",
+ "is-path-inside": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-npm": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz",
+ "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
+ "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
+ "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/json": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/json/-/json-11.0.0.tgz",
+ "integrity": "sha512-N/ITv3Yw9Za8cGxuQqSqrq6RHnlaHWZkAFavcfpH/R52522c26EbihMxnY7A1chxfXJ4d+cEFIsyTgfi9GihrA==",
+ "dev": true,
+ "bin": {
+ "json": "lib/json.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ky": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/ky/-/ky-1.8.1.tgz",
+ "integrity": "sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/ky?sponsor=1"
+ }
+ },
+ "node_modules/latest-version": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz",
+ "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==",
+ "license": "MIT",
+ "dependencies": {
+ "package-json": "^10.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/leac": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
+ "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/load-json-file": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+ "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^4.0.0",
+ "pify": "^3.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/load-json-file/node_modules/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loose-envify/node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/loupe": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz",
+ "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lowlight": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.0.0",
+ "highlight.js": "~11.11.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/memfs": {
+ "version": "4.17.2",
+ "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz",
+ "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/json-pack": "^1.0.3",
+ "@jsonjoy.com/util": "^1.3.0",
+ "tree-dump": "^1.0.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ }
+ },
+ "node_modules/memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/module-details-from-path": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
+ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
+ "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^7.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-package-data/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/npm-run-all": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
+ "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "chalk": "^2.4.1",
+ "cross-spawn": "^6.0.5",
+ "memorystream": "^0.3.1",
+ "minimatch": "^3.0.4",
+ "pidtree": "^0.3.0",
+ "read-pkg": "^3.0.0",
+ "shell-quote": "^1.6.1",
+ "string.prototype.padend": "^3.0.0"
+ },
+ "bin": {
+ "npm-run-all": "bin/npm-run-all/index.js",
+ "run-p": "bin/run-p/index.js",
+ "run-s": "bin/run-s/index.js"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/npm-run-all/node_modules/cross-spawn": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
+ "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/npm-run-all/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/read-pkg": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+ "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "load-json-file": "^4.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.20",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
+ "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open": {
+ "version": "10.1.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
+ "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/openai": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmmirror.com/openai/-/openai-5.8.1.tgz",
+ "integrity": "sha512-+qp4vQjJs43pzMSb6quTYslOhVE0c0c7j4YMoEks83BnusG23UrsWn3Hey6/8mwYadY05KipLvbp+PTO4jxO9w==",
+ "license": "Apache-2.0",
+ "bin": {
+ "openai": "bin/cli"
+ },
+ "peerDependencies": {
+ "ws": "^8.18.0",
+ "zod": "^3.23.8"
+ },
+ "peerDependenciesMeta": {
+ "ws": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz",
+ "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==",
+ "license": "MIT",
+ "dependencies": {
+ "ky": "^1.2.0",
+ "registry-auth-token": "^5.0.2",
+ "registry-url": "^6.0.1",
+ "semver": "^7.6.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/package-json/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
+ "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "index-to-position": "^1.1.0",
+ "type-fest": "^4.39.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-json/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/parseley": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
+ "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
+ "license": "MIT",
+ "dependencies": {
+ "leac": "^0.6.0",
+ "peberminta": "^0.9.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/patch-console": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
+ "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/peberminta": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
+ "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
+ "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
+ "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "30.0.2",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
+ "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "30.0.1",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format/node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/proto-list": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+ "license": "ISC"
+ },
+ "node_modules/protobufjs": {
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
+ "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pupa": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz",
+ "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==",
+ "license": "MIT",
+ "dependencies": {
+ "escape-goat": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "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"
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.6.3",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
+ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-devtools-core": {
+ "version": "4.28.5",
+ "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.5.tgz",
+ "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "shell-quote": "^1.6.1",
+ "ws": "^7"
+ }
+ },
+ "node_modules/react-devtools-core/node_modules/ws": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
+ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.26.0"
+ },
+ "peerDependencies": {
+ "react": "^19.1.0"
+ }
+ },
+ "node_modules/react-dom/node_modules/scheduler": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-reconciler": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.32.0.tgz",
+ "integrity": "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.26.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^19.1.0"
+ }
+ },
+ "node_modules/react-reconciler/node_modules/scheduler": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "license": "MIT"
+ },
+ "node_modules/read-package-up": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz",
+ "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up-simple": "^1.0.0",
+ "read-pkg": "^9.0.0",
+ "type-fest": "^4.6.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-package-up/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
+ "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/normalize-package-data": "^2.4.3",
+ "normalize-package-data": "^6.0.0",
+ "parse-json": "^8.0.0",
+ "type-fest": "^4.6.0",
+ "unicorn-magic": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/registry-auth-token": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz",
+ "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==",
+ "license": "MIT",
+ "dependencies": {
+ "@pnpm/npm-conf": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/registry-url": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz",
+ "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "rc": "1.2.8"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-in-the-middle": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
+ "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "module-details-from-path": "^1.0.3",
+ "resolve": "^1.22.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.5"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+ "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz",
+ "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.44.0",
+ "@rollup/rollup-android-arm64": "4.44.0",
+ "@rollup/rollup-darwin-arm64": "4.44.0",
+ "@rollup/rollup-darwin-x64": "4.44.0",
+ "@rollup/rollup-freebsd-arm64": "4.44.0",
+ "@rollup/rollup-freebsd-x64": "4.44.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.44.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.44.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.44.0",
+ "@rollup/rollup-linux-arm64-musl": "4.44.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.44.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.44.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.44.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.44.0",
+ "@rollup/rollup-linux-x64-gnu": "4.44.0",
+ "@rollup/rollup-linux-x64-musl": "4.44.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.44.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.44.0",
+ "@rollup/rollup-win32-x64-msvc": "4.44.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/run-applescript": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
+ "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "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",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "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"
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/selderee": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
+ "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
+ "license": "MIT",
+ "dependencies": {
+ "parseley": "^0.12.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/killymxi"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/shimmer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/simple-git": {
+ "version": "3.28.0",
+ "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz",
+ "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@kwsites/file-exists": "^1.1.1",
+ "@kwsites/promise-deferred": "^1.1.1",
+ "debug": "^4.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/steveukx/git-js?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
+ "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
+ "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.21",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
+ "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
+ "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.padend": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz",
+ "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
+ "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/stubborn-fs": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz",
+ "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g=="
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
+ "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/terminal-link": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-3.0.0.tgz",
+ "integrity": "sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^5.0.0",
+ "supports-hyperlinks": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/terminal-link/node_modules/ansi-escapes": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz",
+ "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/terminal-link/node_modules/type-fest": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
+ "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^9.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/thingies": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz",
+ "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==",
+ "dev": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=10.18"
+ },
+ "peerDependencies": {
+ "tslib": "^2"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinycolor2": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
+ "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinygradient": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz",
+ "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/tinycolor2": "^1.4.0",
+ "tinycolor2": "^1.0.0"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
+ "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/to-rotated": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/to-rotated/-/to-rotated-1.0.0.tgz",
+ "integrity": "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tree-dump": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz",
+ "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.35.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz",
+ "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.35.0",
+ "@typescript-eslint/parser": "8.35.0",
+ "@typescript-eslint/utils": "8.35.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz",
+ "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/unicorn-magic": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+ "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-notifier": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz",
+ "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boxen": "^8.0.1",
+ "chalk": "^5.3.0",
+ "configstore": "^7.0.0",
+ "is-in-ci": "^1.0.0",
+ "is-installed-globally": "^1.0.0",
+ "is-npm": "^6.0.0",
+ "latest-version": "^9.0.0",
+ "pupa": "^3.1.0",
+ "semver": "^7.6.3",
+ "xdg-basedir": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/yeoman/update-notifier?sponsor=1"
+ }
+ },
+ "node_modules/update-notifier/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/update-notifier/node_modules/boxen": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz",
+ "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-align": "^3.0.1",
+ "camelcase": "^8.0.0",
+ "chalk": "^5.3.0",
+ "cli-boxes": "^3.0.0",
+ "string-width": "^7.2.0",
+ "type-fest": "^4.21.0",
+ "widest-line": "^5.0.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/update-notifier/node_modules/camelcase": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
+ "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/update-notifier/node_modules/chalk": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/update-notifier/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "license": "MIT"
+ },
+ "node_modules/update-notifier/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/update-notifier/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/update-notifier/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/update-notifier/node_modules/widest-line": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
+ "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/update-notifier/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz",
+ "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.6",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.6",
+ "rollup": "^4.40.0",
+ "tinyglobby": "^0.2.14"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.4",
+ "@vitest/mocker": "3.2.4",
+ "@vitest/pretty-format": "^3.2.4",
+ "@vitest/runner": "3.2.4",
+ "@vitest/snapshot": "3.2.4",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.4",
+ "@vitest/ui": "3.2.4",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/when-exit": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz",
+ "integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/widest-line": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
+ "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/window-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/window-size/-/window-size-1.1.1.tgz",
+ "integrity": "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==",
+ "license": "MIT",
+ "dependencies": {
+ "define-property": "^1.0.0",
+ "is-number": "^3.0.0"
+ },
+ "bin": {
+ "window-size": "cli.js"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/window-size/node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xdg-basedir": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
+ "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^7.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoga-layout": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
+ "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
+ "license": "MIT"
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.24.6",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
+ "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ },
+ "packages/cli": {
+ "name": "@qwen/qwen-code",
+ "version": "0.0.1-dev1",
+ "dependencies": {
+ "@qwen/qwen-code-core": "file:../core",
+ "@types/update-notifier": "^6.0.8",
+ "command-exists": "^1.2.9",
+ "diff": "^7.0.0",
+ "dotenv": "^17.1.0",
+ "gaxios": "^7.1.1",
+ "glob": "^10.4.1",
+ "highlight.js": "^11.11.1",
+ "ink": "^6.0.1",
+ "ink-big-text": "^2.0.0",
+ "ink-gradient": "^3.0.0",
+ "ink-link": "^4.1.0",
+ "ink-select-input": "^6.2.0",
+ "ink-spinner": "^5.0.0",
+ "lowlight": "^3.3.0",
+ "mime-types": "^3.0.1",
+ "open": "^10.1.2",
+ "react": "^19.1.0",
+ "read-package-up": "^11.0.0",
+ "shell-quote": "^1.8.3",
+ "string-width": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "strip-json-comments": "^3.1.1",
+ "update-notifier": "^7.3.1",
+ "yargs": "^18.0.0"
+ },
+ "bin": {
+ "qwen": "dist/index.js"
+ },
+ "devDependencies": {
+ "@babel/runtime": "^7.27.6",
+ "@testing-library/react": "^16.3.0",
+ "@types/command-exists": "^1.2.3",
+ "@types/diff": "^7.0.2",
+ "@types/dotenv": "^6.1.1",
+ "@types/node": "^20.11.24",
+ "@types/react": "^19.1.8",
+ "@types/react-dom": "^19.1.6",
+ "@types/semver": "^7.7.0",
+ "@types/shell-quote": "^1.7.5",
+ "@types/yargs": "^17.0.32",
+ "ink-testing-library": "^4.0.0",
+ "jsdom": "^26.1.0",
+ "pretty-format": "^30.0.2",
+ "react-dom": "^19.1.0",
+ "typescript": "^5.3.3",
+ "vitest": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "packages/cli/node_modules/@testing-library/dom": {
+ "version": "10.4.0",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "chalk": "^4.1.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "packages/cli/node_modules/@testing-library/dom/node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "packages/cli/node_modules/@testing-library/react": {
+ "version": "16.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "packages/cli/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "packages/cli/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "packages/cli/node_modules/aria-query": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "packages/cli/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "license": "MIT"
+ },
+ "packages/cli/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "packages/cli/node_modules/string-width": {
+ "version": "7.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "packages/core": {
+ "name": "@qwen/qwen-code-core",
+ "version": "0.0.1-dev1",
+ "dependencies": {
+ "@google/genai": "1.8.0",
+ "@modelcontextprotocol/sdk": "^1.11.0",
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.52.0",
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.52.0",
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.52.0",
+ "@opentelemetry/instrumentation-http": "^0.52.0",
+ "@opentelemetry/sdk-node": "^0.52.0",
+ "@types/glob": "^8.1.0",
+ "@types/html-to-text": "^9.0.4",
+ "ajv": "^8.17.1",
+ "diff": "^7.0.0",
+ "dotenv": "^17.1.0",
+ "gaxios": "^7.1.1",
+ "glob": "^10.4.5",
+ "google-auth-library": "^9.11.0",
+ "html-to-text": "^9.0.5",
+ "ignore": "^7.0.0",
+ "micromatch": "^4.0.8",
+ "open": "^10.1.2",
+ "openai": "^5.7.0",
+ "shell-quote": "^1.8.3",
+ "simple-git": "^3.28.0",
+ "strip-ansi": "^7.1.0",
+ "undici": "^7.10.0",
+ "ws": "^8.18.0"
+ },
+ "devDependencies": {
+ "@types/diff": "^7.0.2",
+ "@types/dotenv": "^6.1.1",
+ "@types/micromatch": "^4.0.8",
+ "@types/minimatch": "^5.1.2",
+ "@types/ws": "^8.5.10",
+ "typescript": "^5.3.3",
+ "vitest": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "packages/core/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "packages/core/node_modules/ignore": {
+ "version": "7.0.5",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "packages/core/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "packages/vscode-ide-companion": {
+ "name": "@qwen/qwen-code-vscode-ide-companion",
+ "version": "0.0.1-dev1",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.15.1",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.3",
+ "@types/node": "20.x",
+ "@types/vscode": "^1.101.0",
+ "@typescript-eslint/eslint-plugin": "^8.31.1",
+ "@typescript-eslint/parser": "^8.31.1",
+ "esbuild": "^0.25.3",
+ "eslint": "^9.25.1",
+ "npm-run-all": "^4.1.5",
+ "typescript": "^5.8.3"
+ },
+ "engines": {
+ "vscode": "^1.101.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..93602c66
--- /dev/null
+++ b/package.json
@@ -0,0 +1,86 @@
+{
+ "name": "@qwen/qwen-code",
+ "version": "0.0.1-dev1",
+ "engines": {
+ "node": ">=20"
+ },
+ "type": "module",
+ "workspaces": [
+ "packages/*"
+ ],
+ "private": "true",
+ "repository": {
+ "type": "git",
+ "url": "git+http://gitlab.alibaba-inc.com/Qwen-Coder/qwen-code.git"
+ },
+ "config": {
+ "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.0.1-dev1"
+ },
+ "scripts": {
+ "start": "node scripts/start.js",
+ "debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
+ "auth:npm": "npx google-artifactregistry-auth",
+ "auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev",
+ "auth": "npm run auth:npm && npm run auth:docker",
+ "generate": "node scripts/generate-git-commit-info.js",
+ "build": "node scripts/build.js",
+ "build:all": "npm run build && npm run build:sandbox",
+ "build:packages": "npm run build --workspaces",
+ "build:sandbox": "node scripts/build_sandbox.js --skip-npm-install-build",
+ "bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
+ "test": "npm run test --workspaces",
+ "test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts",
+ "test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
+ "test:e2e": "npm run test:integration:sandbox:none -- --verbose --keep-output",
+ "test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman",
+ "test:integration:sandbox:none": "GEMINI_SANDBOX=false node integration-tests/run-tests.js",
+ "test:integration:sandbox:docker": "GEMINI_SANDBOX=docker node integration-tests/run-tests.js",
+ "test:integration:sandbox:podman": "GEMINI_SANDBOX=podman node integration-tests/run-tests.js",
+ "lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
+ "lint:fix": "eslint . --fix && eslint integration-tests --fix",
+ "lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0",
+ "format": "prettier --write .",
+ "typecheck": "npm run typecheck --workspaces --if-present",
+ "preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
+ "prepare": "npm run bundle",
+ "prepare:package": "node scripts/prepare-package.js",
+ "release:version": "node scripts/version.js",
+ "telemetry": "node scripts/telemetry.js",
+ "clean": "node scripts/clean.js"
+ },
+ "bin": {
+ "qwen": "bundle/gemini.js"
+ },
+ "files": [
+ "bundle/",
+ "README.md",
+ "LICENSE"
+ ],
+ "devDependencies": {
+ "@types/micromatch": "^4.0.9",
+ "@types/mime-types": "^3.0.1",
+ "@types/minimatch": "^5.1.2",
+ "@types/semver": "^7.7.0",
+ "@types/shell-quote": "^1.7.5",
+ "@vitest/coverage-v8": "^3.1.1",
+ "concurrently": "^9.2.0",
+ "cross-env": "^7.0.3",
+ "esbuild": "^0.25.0",
+ "eslint": "^9.24.0",
+ "eslint-config-prettier": "^10.1.2",
+ "eslint-plugin-import": "^2.31.0",
+ "eslint-plugin-license-header": "^0.8.0",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "glob": "^10.4.5",
+ "globals": "^16.0.0",
+ "json": "^11.0.0",
+ "lodash": "^4.17.21",
+ "memfs": "^4.17.2",
+ "prettier": "^3.5.3",
+ "react-devtools-core": "^4.28.5",
+ "typescript-eslint": "^8.30.1",
+ "vitest": "^3.2.4",
+ "yargs": "^18.0.0"
+ }
+}
diff --git a/packages/cli/index.ts b/packages/cli/index.ts
new file mode 100644
index 00000000..6b7e87a5
--- /dev/null
+++ b/packages/cli/index.ts
@@ -0,0 +1,21 @@
+#!/usr/bin/env node
+
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import './src/gemini.js';
+import { main } from './src/gemini.js';
+
+// --- Global Entry Point ---
+main().catch((error) => {
+ console.error('An unexpected critical error occurred:');
+ if (error instanceof Error) {
+ console.error(error.stack);
+ } else {
+ console.error(String(error));
+ }
+ process.exit(1);
+});
diff --git a/packages/cli/package.json b/packages/cli/package.json
new file mode 100644
index 00000000..48a1861b
--- /dev/null
+++ b/packages/cli/package.json
@@ -0,0 +1,79 @@
+{
+ "name": "@qwen/qwen-code",
+ "version": "0.0.1-dev1",
+ "description": "Gemini CLI",
+ "repository": {
+ "type": "git",
+ "url": "git+http://gitlab.alibaba-inc.com/Qwen-Coder/qwen-code.git"
+ },
+ "type": "module",
+ "main": "dist/index.js",
+ "bin": {
+ "qwen": "dist/index.js"
+ },
+ "scripts": {
+ "build": "node ../../scripts/build_package.js",
+ "start": "node dist/index.js",
+ "debug": "node --inspect-brk dist/index.js",
+ "lint": "eslint . --ext .ts,.tsx",
+ "format": "prettier --write .",
+ "test": "vitest run",
+ "test:ci": "vitest run --coverage",
+ "typecheck": "tsc --noEmit"
+ },
+ "files": [
+ "dist"
+ ],
+ "config": {
+ "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.0.1-dev1"
+ },
+ "dependencies": {
+ "@qwen/qwen-code-core": "file:../core",
+ "@types/update-notifier": "^6.0.8",
+ "command-exists": "^1.2.9",
+ "diff": "^7.0.0",
+ "dotenv": "^17.1.0",
+ "gaxios": "^7.1.1",
+ "glob": "^10.4.1",
+ "highlight.js": "^11.11.1",
+ "ink": "^6.0.1",
+ "ink-big-text": "^2.0.0",
+ "ink-gradient": "^3.0.0",
+ "ink-link": "^4.1.0",
+ "ink-select-input": "^6.2.0",
+ "ink-spinner": "^5.0.0",
+ "lowlight": "^3.3.0",
+ "mime-types": "^3.0.1",
+ "open": "^10.1.2",
+ "react": "^19.1.0",
+ "read-package-up": "^11.0.0",
+ "shell-quote": "^1.8.3",
+ "string-width": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "strip-json-comments": "^3.1.1",
+ "update-notifier": "^7.3.1",
+ "yargs": "^18.0.0"
+ },
+ "devDependencies": {
+ "@babel/runtime": "^7.27.6",
+ "@testing-library/react": "^16.3.0",
+ "@types/command-exists": "^1.2.3",
+ "@types/diff": "^7.0.2",
+ "@types/dotenv": "^6.1.1",
+ "@types/node": "^20.11.24",
+ "@types/react": "^19.1.8",
+ "@types/react-dom": "^19.1.6",
+ "@types/semver": "^7.7.0",
+ "@types/shell-quote": "^1.7.5",
+ "@types/yargs": "^17.0.32",
+ "ink-testing-library": "^4.0.0",
+ "jsdom": "^26.1.0",
+ "pretty-format": "^30.0.2",
+ "react-dom": "^19.1.0",
+ "typescript": "^5.3.3",
+ "vitest": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/packages/cli/src/config/auth.test.ts b/packages/cli/src/config/auth.test.ts
new file mode 100644
index 00000000..e4991a0a
--- /dev/null
+++ b/packages/cli/src/config/auth.test.ts
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { AuthType } from '@qwen/qwen-code-core';
+import { vi } from 'vitest';
+import { validateAuthMethod } from './auth.js';
+
+vi.mock('./settings.js', () => ({
+ loadEnvironment: vi.fn(),
+}));
+
+describe('validateAuthMethod', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ vi.resetModules();
+ process.env = {};
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ it('should return null for LOGIN_WITH_GOOGLE', () => {
+ expect(validateAuthMethod(AuthType.LOGIN_WITH_GOOGLE)).toBeNull();
+ });
+
+ it('should return null for CLOUD_SHELL', () => {
+ expect(validateAuthMethod(AuthType.CLOUD_SHELL)).toBeNull();
+ });
+
+ describe('USE_GEMINI', () => {
+ it('should return null if GEMINI_API_KEY is set', () => {
+ process.env.GEMINI_API_KEY = 'test-key';
+ expect(validateAuthMethod(AuthType.USE_GEMINI)).toBeNull();
+ });
+
+ it('should return an error message if GEMINI_API_KEY is not set', () => {
+ expect(validateAuthMethod(AuthType.USE_GEMINI)).toBe(
+ 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!',
+ );
+ });
+ });
+
+ describe('USE_VERTEX_AI', () => {
+ it('should return null if GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION are set', () => {
+ process.env.GOOGLE_CLOUD_PROJECT = 'test-project';
+ process.env.GOOGLE_CLOUD_LOCATION = 'test-location';
+ expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
+ });
+
+ it('should return null if GOOGLE_API_KEY is set', () => {
+ process.env.GOOGLE_API_KEY = 'test-api-key';
+ expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
+ });
+
+ it('should return an error message if no required environment variables are set', () => {
+ expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBe(
+ 'When using Vertex AI, you must specify either:\n' +
+ 'โข GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
+ 'โข GOOGLE_API_KEY environment variable (if using express mode).\n' +
+ 'Update your environment and try again (no reload needed if using .env)!',
+ );
+ });
+ });
+
+ it('should return an error message for an invalid auth method', () => {
+ expect(validateAuthMethod('invalid-method')).toBe(
+ 'Invalid auth method selected.',
+ );
+ });
+});
diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts
new file mode 100644
index 00000000..b2352318
--- /dev/null
+++ b/packages/cli/src/config/auth.ts
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { AuthType } from '@qwen/qwen-code-core';
+import { loadEnvironment } from './settings.js';
+
+export const validateAuthMethod = (authMethod: string): string | null => {
+ loadEnvironment();
+ if (
+ authMethod === AuthType.LOGIN_WITH_GOOGLE ||
+ authMethod === AuthType.CLOUD_SHELL
+ ) {
+ return null;
+ }
+
+ if (authMethod === AuthType.USE_GEMINI) {
+ if (!process.env.GEMINI_API_KEY) {
+ return 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!';
+ }
+ return null;
+ }
+
+ if (authMethod === AuthType.USE_VERTEX_AI) {
+ const hasVertexProjectLocationConfig =
+ !!process.env.GOOGLE_CLOUD_PROJECT && !!process.env.GOOGLE_CLOUD_LOCATION;
+ const hasGoogleApiKey = !!process.env.GOOGLE_API_KEY;
+ if (!hasVertexProjectLocationConfig && !hasGoogleApiKey) {
+ return (
+ 'When using Vertex AI, you must specify either:\n' +
+ 'โข GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
+ 'โข GOOGLE_API_KEY environment variable (if using express mode).\n' +
+ 'Update your environment and try again (no reload needed if using .env)!'
+ );
+ }
+ return null;
+ }
+
+ if (authMethod === AuthType.USE_OPENAI) {
+ if (!process.env.OPENAI_API_KEY) {
+ return 'OPENAI_API_KEY environment variable not found. You can enter it interactively or add it to your .env file.';
+ }
+ return null;
+ }
+
+ return 'Invalid auth method selected.';
+};
+
+export const setOpenAIApiKey = (apiKey: string): void => {
+ process.env.OPENAI_API_KEY = apiKey;
+};
+
+export const setOpenAIBaseUrl = (baseUrl: string): void => {
+ process.env.OPENAI_BASE_URL = baseUrl;
+};
+
+export const setOpenAIModel = (model: string): void => {
+ process.env.OPENAI_MODEL = model;
+};
diff --git a/packages/cli/src/config/config.integration.test.ts b/packages/cli/src/config/config.integration.test.ts
new file mode 100644
index 00000000..c2ddeb41
--- /dev/null
+++ b/packages/cli/src/config/config.integration.test.ts
@@ -0,0 +1,243 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import * as fs from 'fs';
+import * as path from 'path';
+import { tmpdir } from 'os';
+import {
+ Config,
+ ConfigParameters,
+ ContentGeneratorConfig,
+} from '@qwen/qwen-code-core';
+
+const TEST_CONTENT_GENERATOR_CONFIG: ContentGeneratorConfig = {
+ apiKey: 'test-key',
+ model: 'test-model',
+ userAgent: 'test-agent',
+};
+
+// Mock file discovery service and tool registry
+vi.mock('@qwen/qwen-code-core', async () => {
+ const actual = await vi.importActual('@qwen/qwen-code-core');
+ return {
+ ...actual,
+ FileDiscoveryService: vi.fn().mockImplementation(() => ({
+ initialize: vi.fn(),
+ })),
+ createToolRegistry: vi.fn().mockResolvedValue({}),
+ };
+});
+
+describe('Configuration Integration Tests', () => {
+ let tempDir: string;
+ let originalEnv: NodeJS.ProcessEnv;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(tmpdir(), 'gemini-cli-test-'));
+ originalEnv = { ...process.env };
+ process.env.GEMINI_API_KEY = 'test-api-key';
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ if (fs.existsSync(tempDir)) {
+ fs.rmSync(tempDir, { recursive: true });
+ }
+ });
+
+ describe('File Filtering Configuration', () => {
+ it('should load default file filtering settings', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ fileFilteringRespectGitIgnore: undefined, // Should default to true
+ };
+
+ const config = new Config(configParams);
+
+ expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
+ });
+
+ it('should load custom file filtering settings from configuration', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ fileFiltering: {
+ respectGitIgnore: false,
+ },
+ };
+
+ const config = new Config(configParams);
+
+ expect(config.getFileFilteringRespectGitIgnore()).toBe(false);
+ });
+
+ it('should merge user and workspace file filtering settings', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ fileFilteringRespectGitIgnore: true,
+ };
+
+ const config = new Config(configParams);
+
+ expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
+ });
+ });
+
+ describe('Configuration Integration', () => {
+ it('should handle partial configuration objects gracefully', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ fileFiltering: {
+ respectGitIgnore: false,
+ },
+ };
+
+ const config = new Config(configParams);
+
+ // Specified settings should be applied
+ expect(config.getFileFilteringRespectGitIgnore()).toBe(false);
+ });
+
+ it('should handle empty configuration objects gracefully', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ fileFilteringRespectGitIgnore: undefined,
+ };
+
+ const config = new Config(configParams);
+
+ // All settings should use defaults
+ expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
+ });
+
+ it('should handle missing configuration sections gracefully', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ // Missing fileFiltering configuration
+ };
+
+ const config = new Config(configParams);
+
+ // All git-aware settings should use defaults
+ expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
+ });
+ });
+
+ describe('Real-world Configuration Scenarios', () => {
+ it('should handle a security-focused configuration', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ fileFilteringRespectGitIgnore: true,
+ };
+
+ const config = new Config(configParams);
+
+ expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
+ });
+
+ it('should handle a CI/CD environment configuration', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ fileFiltering: {
+ respectGitIgnore: false,
+ }, // CI might need to see all files
+ };
+
+ const config = new Config(configParams);
+
+ expect(config.getFileFilteringRespectGitIgnore()).toBe(false);
+ });
+ });
+
+ describe('Checkpointing Configuration', () => {
+ it('should enable checkpointing when the setting is true', async () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ checkpointing: true,
+ };
+
+ const config = new Config(configParams);
+
+ expect(config.getCheckpointingEnabled()).toBe(true);
+ });
+ });
+
+ describe('Extension Context Files', () => {
+ it('should have an empty array for extension context files by default', () => {
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ };
+ const config = new Config(configParams);
+ expect(config.getExtensionContextFilePaths()).toEqual([]);
+ });
+
+ it('should correctly store and return extension context file paths', () => {
+ const contextFiles = ['/path/to/file1.txt', '/path/to/file2.js'];
+ const configParams: ConfigParameters = {
+ cwd: '/tmp',
+ contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
+ embeddingModel: 'test-embedding-model',
+ sandbox: false,
+ targetDir: tempDir,
+ debugMode: false,
+ extensionContextFilePaths: contextFiles,
+ };
+ const config = new Config(configParams);
+ expect(config.getExtensionContextFilePaths()).toEqual(contextFiles);
+ });
+ });
+});
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
new file mode 100644
index 00000000..4014b6a2
--- /dev/null
+++ b/packages/cli/src/config/config.test.ts
@@ -0,0 +1,1009 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import * as os from 'os';
+import { loadCliConfig, parseArguments } from './config.js';
+import { Settings } from './settings.js';
+import { Extension } from './extension.js';
+import * as ServerConfig from '@qwen/qwen-code-core';
+import {
+ TelemetryTarget,
+ ConfigParameters,
+ DEFAULT_TELEMETRY_TARGET,
+} from '@qwen/qwen-code-core';
+
+vi.mock('os', async (importOriginal) => {
+ const actualOs = await importOriginal();
+ return {
+ ...actualOs,
+ homedir: vi.fn(() => '/mock/home/user'),
+ };
+});
+
+vi.mock('open', () => ({
+ default: vi.fn(),
+}));
+
+vi.mock('read-package-up', () => ({
+ readPackageUp: vi.fn(() =>
+ Promise.resolve({ packageJson: { version: 'test-version' } }),
+ ),
+}));
+
+vi.mock('@qwen/qwen-code-core', async () => {
+ const actualServer = await vi.importActual(
+ '@qwen/qwen-code-core',
+ );
+ return {
+ ...actualServer,
+ loadEnvironment: vi.fn(),
+ loadServerHierarchicalMemory: vi.fn(
+ (cwd, debug, fileService, extensionPaths) =>
+ Promise.resolve({
+ memoryContent: extensionPaths?.join(',') || '',
+ fileCount: extensionPaths?.length || 0,
+ }),
+ ),
+ Config: class MockConfig extends actualServer.Config {
+ private enableOpenAILogging: boolean;
+
+ constructor(params: ConfigParameters) {
+ super(params);
+ this.enableOpenAILogging = params.enableOpenAILogging ?? false;
+ }
+
+ getEnableOpenAILogging(): boolean {
+ return this.enableOpenAILogging;
+ }
+
+ // Override other methods to ensure they work correctly
+ getShowMemoryUsage(): boolean {
+ return (
+ (this as unknown as { showMemoryUsage?: boolean }).showMemoryUsage ??
+ false
+ );
+ }
+
+ getTelemetryEnabled(): boolean {
+ return (
+ (this as unknown as { telemetrySettings?: { enabled?: boolean } })
+ .telemetrySettings?.enabled ?? false
+ );
+ }
+
+ getTelemetryLogPromptsEnabled(): boolean {
+ return (
+ (this as unknown as { telemetrySettings?: { logPrompts?: boolean } })
+ .telemetrySettings?.logPrompts ?? true
+ );
+ }
+
+ getTelemetryOtlpEndpoint(): string {
+ return (
+ (this as unknown as { telemetrySettings?: { otlpEndpoint?: string } })
+ .telemetrySettings?.otlpEndpoint ?? 'http://localhost:4317'
+ );
+ }
+
+ getTelemetryTarget(): TelemetryTarget {
+ return (
+ (
+ this as unknown as {
+ telemetrySettings?: { target?: TelemetryTarget };
+ }
+ ).telemetrySettings?.target ?? DEFAULT_TELEMETRY_TARGET
+ );
+ }
+ },
+ };
+});
+
+describe('parseArguments', () => {
+ const originalArgv = process.argv;
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ });
+
+ it('should throw an error when both --prompt and --prompt-interactive are used together', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '--prompt',
+ 'test prompt',
+ '--prompt-interactive',
+ 'interactive prompt',
+ ];
+
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
+ throw new Error('process.exit called');
+ });
+
+ const mockConsoleError = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ await expect(parseArguments()).rejects.toThrow('process.exit called');
+
+ expect(mockConsoleError).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together',
+ ),
+ );
+
+ mockExit.mockRestore();
+ mockConsoleError.mockRestore();
+ });
+
+ it('should throw an error when using short flags -p and -i together', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '-p',
+ 'test prompt',
+ '-i',
+ 'interactive prompt',
+ ];
+
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
+ throw new Error('process.exit called');
+ });
+
+ const mockConsoleError = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ await expect(parseArguments()).rejects.toThrow('process.exit called');
+
+ expect(mockConsoleError).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together',
+ ),
+ );
+
+ mockExit.mockRestore();
+ mockConsoleError.mockRestore();
+ });
+
+ it('should allow --prompt without --prompt-interactive', async () => {
+ process.argv = ['node', 'script.js', '--prompt', 'test prompt'];
+ const argv = await parseArguments();
+ expect(argv.prompt).toBe('test prompt');
+ expect(argv.promptInteractive).toBeUndefined();
+ });
+
+ it('should allow --prompt-interactive without --prompt', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '--prompt-interactive',
+ 'interactive prompt',
+ ];
+ const argv = await parseArguments();
+ expect(argv.promptInteractive).toBe('interactive prompt');
+ expect(argv.prompt).toBeUndefined();
+ });
+
+ it('should allow -i flag as alias for --prompt-interactive', async () => {
+ process.argv = ['node', 'script.js', '-i', 'interactive prompt'];
+ const argv = await parseArguments();
+ expect(argv.promptInteractive).toBe('interactive prompt');
+ expect(argv.prompt).toBeUndefined();
+ });
+});
+
+describe('loadCliConfig', () => {
+ const originalArgv = process.argv;
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ process.env.GEMINI_API_KEY = 'test-api-key'; // Ensure API key is set for tests
+ });
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ process.env = originalEnv;
+ vi.restoreAllMocks();
+ });
+
+ it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => {
+ process.argv = ['node', 'script.js', '--show-memory-usage'];
+ const argv = await parseArguments();
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getShowMemoryUsage()).toBe(true);
+ });
+
+ it('should set showMemoryUsage to false when --memory flag is not present', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getShowMemoryUsage()).toBe(false);
+ });
+
+ it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { showMemoryUsage: false };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getShowMemoryUsage()).toBe(false);
+ });
+
+ it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => {
+ process.argv = ['node', 'script.js', '--show-memory-usage'];
+ const argv = await parseArguments();
+ const settings: Settings = { showMemoryUsage: false };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getShowMemoryUsage()).toBe(true);
+ });
+});
+
+describe('loadCliConfig telemetry', () => {
+ const originalArgv = process.argv;
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ process.env.GEMINI_API_KEY = 'test-api-key';
+ });
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ process.env = originalEnv;
+ vi.restoreAllMocks();
+ });
+
+ it('should set telemetry to false by default when no flag or setting is present', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryEnabled()).toBe(false);
+ });
+
+ it('should set telemetry to true when --telemetry flag is present', async () => {
+ process.argv = ['node', 'script.js', '--telemetry'];
+ const argv = await parseArguments();
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryEnabled()).toBe(true);
+ });
+
+ it('should set telemetry to false when --no-telemetry flag is present', async () => {
+ process.argv = ['node', 'script.js', '--no-telemetry'];
+ const argv = await parseArguments();
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryEnabled()).toBe(false);
+ });
+
+ it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { enabled: true } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryEnabled()).toBe(true);
+ });
+
+ it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { enabled: false } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryEnabled()).toBe(false);
+ });
+
+ it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => {
+ process.argv = ['node', 'script.js', '--telemetry'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { enabled: false } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryEnabled()).toBe(true);
+ });
+
+ it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => {
+ process.argv = ['node', 'script.js', '--no-telemetry'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { enabled: true } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryEnabled()).toBe(false);
+ });
+
+ it('should use telemetry OTLP endpoint from settings if CLI flag is not present', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = {
+ telemetry: { otlpEndpoint: 'http://settings.example.com' },
+ };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryOtlpEndpoint()).toBe(
+ 'http://settings.example.com',
+ );
+ });
+
+ it('should prioritize --telemetry-otlp-endpoint CLI flag over settings', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '--telemetry-otlp-endpoint',
+ 'http://cli.example.com',
+ ];
+ const argv = await parseArguments();
+ const settings: Settings = {
+ telemetry: { otlpEndpoint: 'http://settings.example.com' },
+ };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryOtlpEndpoint()).toBe('http://cli.example.com');
+ });
+
+ it('should use default endpoint if no OTLP endpoint is provided via CLI or settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { enabled: true } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317');
+ });
+
+ it('should use telemetry target from settings if CLI flag is not present', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = {
+ telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
+ };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryTarget()).toBe(
+ ServerConfig.DEFAULT_TELEMETRY_TARGET,
+ );
+ });
+
+ it('should prioritize --telemetry-target CLI flag over settings', async () => {
+ process.argv = ['node', 'script.js', '--telemetry-target', 'gcp'];
+ const argv = await parseArguments();
+ const settings: Settings = {
+ telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
+ };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryTarget()).toBe('gcp');
+ });
+
+ it('should use default target if no target is provided via CLI or settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { enabled: true } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryTarget()).toBe(
+ ServerConfig.DEFAULT_TELEMETRY_TARGET,
+ );
+ });
+
+ it('should use telemetry log prompts from settings if CLI flag is not present', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { logPrompts: false } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
+ });
+
+ it('should prioritize --telemetry-log-prompts CLI flag (true) over settings (false)', async () => {
+ process.argv = ['node', 'script.js', '--telemetry-log-prompts'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { logPrompts: false } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
+ });
+
+ it('should prioritize --no-telemetry-log-prompts CLI flag (false) over settings (true)', async () => {
+ process.argv = ['node', 'script.js', '--no-telemetry-log-prompts'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { logPrompts: true } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
+ });
+
+ it('should use default log prompts (true) if no value is provided via CLI or settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { telemetry: { enabled: true } };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
+ });
+
+ it('should set enableOpenAILogging to true when --openai-logging flag is present', async () => {
+ const settings: Settings = {};
+ const argv = await parseArguments();
+ argv.openaiLogging = true;
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(
+ (
+ config as unknown as { getEnableOpenAILogging(): boolean }
+ ).getEnableOpenAILogging(),
+ ).toBe(true);
+ });
+
+ it('should set enableOpenAILogging to false when --openai-logging flag is not present', async () => {
+ const settings: Settings = {};
+ const argv = await parseArguments();
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(
+ (
+ config as unknown as { getEnableOpenAILogging(): boolean }
+ ).getEnableOpenAILogging(),
+ ).toBe(false);
+ });
+
+ it('should use enableOpenAILogging value from settings if CLI flag is not present (settings true)', async () => {
+ const settings: Settings = { enableOpenAILogging: true };
+ const argv = await parseArguments();
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(
+ (
+ config as unknown as { getEnableOpenAILogging(): boolean }
+ ).getEnableOpenAILogging(),
+ ).toBe(true);
+ });
+
+ it('should use enableOpenAILogging value from settings if CLI flag is not present (settings false)', async () => {
+ const settings: Settings = { enableOpenAILogging: false };
+ const argv = await parseArguments();
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(
+ (
+ config as unknown as { getEnableOpenAILogging(): boolean }
+ ).getEnableOpenAILogging(),
+ ).toBe(false);
+ });
+
+ it('should prioritize --openai-logging CLI flag (true) over settings (false)', async () => {
+ const settings: Settings = { enableOpenAILogging: false };
+ const argv = await parseArguments();
+ argv.openaiLogging = true;
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(
+ (
+ config as unknown as { getEnableOpenAILogging(): boolean }
+ ).getEnableOpenAILogging(),
+ ).toBe(true);
+ });
+
+ it('should prioritize --openai-logging CLI flag (false) over settings (true)', async () => {
+ const settings: Settings = { enableOpenAILogging: true };
+ const argv = await parseArguments();
+ argv.openaiLogging = false;
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(
+ (
+ config as unknown as { getEnableOpenAILogging(): boolean }
+ ).getEnableOpenAILogging(),
+ ).toBe(false);
+ });
+});
+
+describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ // Other common mocks would be reset here.
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
+ process.argv = ['node', 'script.js'];
+ const settings: Settings = {};
+ const extensions: Extension[] = [
+ {
+ config: {
+ name: 'ext1',
+ version: '1.0.0',
+ },
+ contextFiles: ['/path/to/ext1/QWEN.md'],
+ },
+ {
+ config: {
+ name: 'ext2',
+ version: '1.0.0',
+ },
+ contextFiles: [],
+ },
+ {
+ config: {
+ name: 'ext3',
+ version: '1.0.0',
+ },
+ contextFiles: [
+ '/path/to/ext3/context1.md',
+ '/path/to/ext3/context2.md',
+ ],
+ },
+ ];
+ const argv = await parseArguments();
+ await loadCliConfig(settings, extensions, 'session-id', argv);
+ expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
+ expect.any(String),
+ false,
+ expect.any(Object),
+ [
+ '/path/to/ext1/QWEN.md',
+ '/path/to/ext3/context1.md',
+ '/path/to/ext3/context2.md',
+ ],
+ );
+ });
+
+ // NOTE TO FUTURE DEVELOPERS:
+ // To re-enable tests for loadHierarchicalGeminiMemory, ensure that:
+ // 1. os.homedir() is reliably mocked *before* the config.ts module is loaded
+ // and its functions (which use os.homedir()) are called.
+ // 2. fs/promises and fs mocks correctly simulate file/directory existence,
+ // readability, and content based on paths derived from the mocked os.homedir().
+ // 3. Spies on console functions (for logger output) are correctly set up if needed.
+ // Example of a previously failing test structure:
+ /*
+ it('should correctly use mocked homedir for global path', async () => {
+ const MOCK_GEMINI_DIR_LOCAL = path.join('/mock/home/user', '.qwen');
+ const MOCK_GLOBAL_PATH_LOCAL = path.join(MOCK_GEMINI_DIR_LOCAL, 'QWEN.md');
+ mockFs({
+ [MOCK_GLOBAL_PATH_LOCAL]: { type: 'file', content: 'GlobalContentOnly' }
+ });
+ const memory = await loadHierarchicalGeminiMemory("/some/other/cwd", false);
+ expect(memory).toBe('GlobalContentOnly');
+ expect(vi.mocked(os.homedir)).toHaveBeenCalled();
+ expect(fsPromises.readFile).toHaveBeenCalledWith(MOCK_GLOBAL_PATH_LOCAL, 'utf-8');
+ });
+ */
+});
+
+describe('mergeMcpServers', () => {
+ it('should not modify the original settings object', async () => {
+ const settings: Settings = {
+ mcpServers: {
+ 'test-server': {
+ url: 'http://localhost:8080',
+ },
+ },
+ };
+ const extensions: Extension[] = [
+ {
+ config: {
+ name: 'ext1',
+ version: '1.0.0',
+ mcpServers: {
+ 'ext1-server': {
+ url: 'http://localhost:8081',
+ },
+ },
+ },
+ contextFiles: [],
+ },
+ ];
+ const originalSettings = JSON.parse(JSON.stringify(settings));
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ await loadCliConfig(settings, extensions, 'test-session', argv);
+ expect(settings).toEqual(originalSettings);
+ });
+});
+
+describe('mergeExcludeTools', () => {
+ it('should merge excludeTools from settings and extensions', async () => {
+ const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
+ const extensions: Extension[] = [
+ {
+ config: {
+ name: 'ext1',
+ version: '1.0.0',
+ excludeTools: ['tool3', 'tool4'],
+ },
+ contextFiles: [],
+ },
+ {
+ config: {
+ name: 'ext2',
+ version: '1.0.0',
+ excludeTools: ['tool5'],
+ },
+ contextFiles: [],
+ },
+ ];
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(
+ settings,
+ extensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExcludeTools()).toEqual(
+ expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4', 'tool5']),
+ );
+ expect(config.getExcludeTools()).toHaveLength(5);
+ });
+
+ it('should handle overlapping excludeTools between settings and extensions', async () => {
+ const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
+ const extensions: Extension[] = [
+ {
+ config: {
+ name: 'ext1',
+ version: '1.0.0',
+ excludeTools: ['tool2', 'tool3'],
+ },
+ contextFiles: [],
+ },
+ ];
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(
+ settings,
+ extensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExcludeTools()).toEqual(
+ expect.arrayContaining(['tool1', 'tool2', 'tool3']),
+ );
+ expect(config.getExcludeTools()).toHaveLength(3);
+ });
+
+ it('should handle overlapping excludeTools between extensions', async () => {
+ const settings: Settings = { excludeTools: ['tool1'] };
+ const extensions: Extension[] = [
+ {
+ config: {
+ name: 'ext1',
+ version: '1.0.0',
+ excludeTools: ['tool2', 'tool3'],
+ },
+ contextFiles: [],
+ },
+ {
+ config: {
+ name: 'ext2',
+ version: '1.0.0',
+ excludeTools: ['tool3', 'tool4'],
+ },
+ contextFiles: [],
+ },
+ ];
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(
+ settings,
+ extensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExcludeTools()).toEqual(
+ expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4']),
+ );
+ expect(config.getExcludeTools()).toHaveLength(4);
+ });
+
+ it('should return an empty array when no excludeTools are specified', async () => {
+ const settings: Settings = {};
+ const extensions: Extension[] = [];
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(
+ settings,
+ extensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExcludeTools()).toEqual([]);
+ });
+
+ it('should handle settings with excludeTools but no extensions', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
+ const extensions: Extension[] = [];
+ const config = await loadCliConfig(
+ settings,
+ extensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExcludeTools()).toEqual(
+ expect.arrayContaining(['tool1', 'tool2']),
+ );
+ expect(config.getExcludeTools()).toHaveLength(2);
+ });
+
+ it('should handle extensions with excludeTools but no settings', async () => {
+ const settings: Settings = {};
+ const extensions: Extension[] = [
+ {
+ config: {
+ name: 'ext1',
+ version: '1.0.0',
+ excludeTools: ['tool1', 'tool2'],
+ },
+ contextFiles: [],
+ },
+ ];
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(
+ settings,
+ extensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExcludeTools()).toEqual(
+ expect.arrayContaining(['tool1', 'tool2']),
+ );
+ expect(config.getExcludeTools()).toHaveLength(2);
+ });
+
+ it('should not modify the original settings object', async () => {
+ const settings: Settings = { excludeTools: ['tool1'] };
+ const extensions: Extension[] = [
+ {
+ config: {
+ name: 'ext1',
+ version: '1.0.0',
+ excludeTools: ['tool2'],
+ },
+ contextFiles: [],
+ },
+ ];
+ const originalSettings = JSON.parse(JSON.stringify(settings));
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ await loadCliConfig(settings, extensions, 'test-session', argv);
+ expect(settings).toEqual(originalSettings);
+ });
+});
+
+describe('loadCliConfig with allowed-mcp-server-names', () => {
+ const originalArgv = process.argv;
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ process.env.GEMINI_API_KEY = 'test-api-key';
+ });
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ process.env = originalEnv;
+ vi.restoreAllMocks();
+ });
+
+ const baseSettings: Settings = {
+ mcpServers: {
+ server1: { url: 'http://localhost:8080' },
+ server2: { url: 'http://localhost:8081' },
+ server3: { url: 'http://localhost:8082' },
+ },
+ };
+
+ it('should allow all MCP servers if the flag is not provided', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
+ expect(config.getMcpServers()).toEqual(baseSettings.mcpServers);
+ });
+
+ it('should allow only the specified MCP server', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '--allowed-mcp-server-names',
+ 'server1',
+ ];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
+ expect(config.getMcpServers()).toEqual({
+ server1: { url: 'http://localhost:8080' },
+ });
+ });
+
+ it('should allow multiple specified MCP servers', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '--allowed-mcp-server-names',
+ 'server1',
+ '--allowed-mcp-server-names',
+ 'server3',
+ ];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
+ expect(config.getMcpServers()).toEqual({
+ server1: { url: 'http://localhost:8080' },
+ server3: { url: 'http://localhost:8082' },
+ });
+ });
+
+ it('should handle server names that do not exist', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '--allowed-mcp-server-names',
+ 'server1',
+ '--allowed-mcp-server-names',
+ 'server4',
+ ];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
+ expect(config.getMcpServers()).toEqual({
+ server1: { url: 'http://localhost:8080' },
+ });
+ });
+
+ it('should allow no MCP servers if the flag is provided but empty', async () => {
+ process.argv = ['node', 'script.js', '--allowed-mcp-server-names', ''];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(baseSettings, [], 'test-session', argv);
+ expect(config.getMcpServers()).toEqual({});
+ });
+});
+
+describe('loadCliConfig extensions', () => {
+ const mockExtensions: Extension[] = [
+ {
+ config: { name: 'ext1', version: '1.0.0' },
+ contextFiles: ['/path/to/ext1.md'],
+ },
+ {
+ config: { name: 'ext2', version: '1.0.0' },
+ contextFiles: ['/path/to/ext2.md'],
+ },
+ ];
+
+ it('should not filter extensions if --extensions flag is not used', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = {};
+ const config = await loadCliConfig(
+ settings,
+ mockExtensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExtensionContextFilePaths()).toEqual([
+ '/path/to/ext1.md',
+ '/path/to/ext2.md',
+ ]);
+ });
+
+ it('should filter extensions if --extensions flag is used', async () => {
+ process.argv = ['node', 'script.js', '--extensions', 'ext1'];
+ const argv = await parseArguments();
+ const settings: Settings = {};
+ const config = await loadCliConfig(
+ settings,
+ mockExtensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExtensionContextFilePaths()).toEqual(['/path/to/ext1.md']);
+ });
+});
+
+describe('loadCliConfig ideMode', () => {
+ const originalArgv = process.argv;
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ process.env.GEMINI_API_KEY = 'test-api-key';
+ // Explicitly delete TERM_PROGRAM and SANDBOX before each test
+ delete process.env.TERM_PROGRAM;
+ delete process.env.SANDBOX;
+ });
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ process.env = originalEnv;
+ vi.restoreAllMocks();
+ });
+
+ it('should be false by default', async () => {
+ process.argv = ['node', 'script.js'];
+ const settings: Settings = {};
+ const argv = await parseArguments();
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(false);
+ });
+
+ it('should be false if --ide-mode is true but TERM_PROGRAM is not vscode', async () => {
+ process.argv = ['node', 'script.js', '--ide-mode'];
+ const settings: Settings = {};
+ const argv = await parseArguments();
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(false);
+ });
+
+ it('should be false if settings.ideMode is true but TERM_PROGRAM is not vscode', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { ideMode: true };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(false);
+ });
+
+ it('should be true when --ide-mode is set and TERM_PROGRAM is vscode', async () => {
+ process.argv = ['node', 'script.js', '--ide-mode'];
+ const argv = await parseArguments();
+ process.env.TERM_PROGRAM = 'vscode';
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(true);
+ });
+
+ it('should be true when settings.ideMode is true and TERM_PROGRAM is vscode', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ process.env.TERM_PROGRAM = 'vscode';
+ const settings: Settings = { ideMode: true };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(true);
+ });
+
+ it('should prioritize --ide-mode (true) over settings (false) when TERM_PROGRAM is vscode', async () => {
+ process.argv = ['node', 'script.js', '--ide-mode'];
+ const argv = await parseArguments();
+ process.env.TERM_PROGRAM = 'vscode';
+ const settings: Settings = { ideMode: false };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(true);
+ });
+
+ it('should prioritize --no-ide-mode (false) over settings (true) even when TERM_PROGRAM is vscode', async () => {
+ process.argv = ['node', 'script.js', '--no-ide-mode'];
+ const argv = await parseArguments();
+ process.env.TERM_PROGRAM = 'vscode';
+ const settings: Settings = { ideMode: true };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(false);
+ });
+
+ it('should be false when --ide-mode is true, TERM_PROGRAM is vscode, but SANDBOX is set', async () => {
+ process.argv = ['node', 'script.js', '--ide-mode'];
+ const argv = await parseArguments();
+ process.env.TERM_PROGRAM = 'vscode';
+ process.env.SANDBOX = 'true';
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(false);
+ });
+
+ it('should be false when settings.ideMode is true, TERM_PROGRAM is vscode, but SANDBOX is set', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ process.env.TERM_PROGRAM = 'vscode';
+ process.env.SANDBOX = 'true';
+ const settings: Settings = { ideMode: true };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(false);
+ });
+
+ it('should add __ide_server when ideMode is true', async () => {
+ process.argv = ['node', 'script.js', '--ide-mode'];
+ const argv = await parseArguments();
+ process.env.TERM_PROGRAM = 'vscode';
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getIdeMode()).toBe(true);
+ const mcpServers = config.getMcpServers();
+ expect(mcpServers['_ide_server']).toBeDefined();
+ expect(mcpServers['_ide_server'].httpUrl).toBe('http://localhost:3000/mcp');
+ expect(mcpServers['_ide_server'].description).toBe('IDE connection');
+ expect(mcpServers['_ide_server'].trust).toBe(false);
+ });
+});
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
new file mode 100644
index 00000000..b96944b3
--- /dev/null
+++ b/packages/cli/src/config/config.ts
@@ -0,0 +1,429 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import yargs from 'yargs/yargs';
+import { hideBin } from 'yargs/helpers';
+import process from 'node:process';
+import {
+ Config,
+ loadServerHierarchicalMemory,
+ setGeminiMdFilename as setServerGeminiMdFilename,
+ getCurrentGeminiMdFilename,
+ ApprovalMode,
+ DEFAULT_GEMINI_MODEL,
+ DEFAULT_GEMINI_EMBEDDING_MODEL,
+ FileDiscoveryService,
+ TelemetryTarget,
+ MCPServerConfig,
+} from '@qwen/qwen-code-core';
+import { Settings } from './settings.js';
+
+import { Extension, filterActiveExtensions } from './extension.js';
+import { getCliVersion } from '../utils/version.js';
+import { loadSandboxConfig } from './sandboxConfig.js';
+
+// Simple console logger for now - replace with actual logger if available
+const logger = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ debug: (...args: any[]) => console.debug('[DEBUG]', ...args),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ warn: (...args: any[]) => console.warn('[WARN]', ...args),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ error: (...args: any[]) => console.error('[ERROR]', ...args),
+};
+
+export interface CliArgs {
+ model: string | undefined;
+ sandbox: boolean | string | undefined;
+ sandboxImage: string | undefined;
+ debug: boolean | undefined;
+ prompt: string | undefined;
+ promptInteractive: string | undefined;
+ allFiles: boolean | undefined;
+ all_files: boolean | undefined;
+ showMemoryUsage: boolean | undefined;
+ show_memory_usage: boolean | undefined;
+ yolo: boolean | undefined;
+ telemetry: boolean | undefined;
+ checkpointing: boolean | undefined;
+ telemetryTarget: string | undefined;
+ telemetryOtlpEndpoint: string | undefined;
+ telemetryLogPrompts: boolean | undefined;
+ allowedMcpServerNames: string[] | undefined;
+ extensions: string[] | undefined;
+ listExtensions: boolean | undefined;
+ ideMode: boolean | undefined;
+ openaiLogging: boolean | undefined;
+ openaiApiKey: string | undefined;
+ openaiBaseUrl: string | undefined;
+}
+
+export async function parseArguments(): Promise {
+ const yargsInstance = yargs(hideBin(process.argv))
+ .scriptName('qwen')
+ .usage(
+ '$0 [options]',
+ 'Qwen Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode',
+ )
+ .option('model', {
+ alias: 'm',
+ type: 'string',
+ description: `Model`,
+ default: process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
+ })
+ .option('prompt', {
+ alias: 'p',
+ type: 'string',
+ description: 'Prompt. Appended to input on stdin (if any).',
+ })
+ .option('prompt-interactive', {
+ alias: 'i',
+ type: 'string',
+ description:
+ 'Execute the provided prompt and continue in interactive mode',
+ })
+ .option('sandbox', {
+ alias: 's',
+ type: 'boolean',
+ description: 'Run in sandbox?',
+ })
+ .option('sandbox-image', {
+ type: 'string',
+ description: 'Sandbox image URI.',
+ })
+ .option('debug', {
+ alias: 'd',
+ type: 'boolean',
+ description: 'Run in debug mode?',
+ default: false,
+ })
+ .option('all-files', {
+ alias: ['a'],
+ type: 'boolean',
+ description: 'Include ALL files in context?',
+ default: false,
+ })
+ .option('all_files', {
+ type: 'boolean',
+ description: 'Include ALL files in context?',
+ default: false,
+ })
+ .deprecateOption(
+ 'all_files',
+ 'Use --all-files instead. We will be removing --all_files in the coming weeks.',
+ )
+ .option('show-memory-usage', {
+ type: 'boolean',
+ description: 'Show memory usage in status bar',
+ default: false,
+ })
+ .option('show_memory_usage', {
+ type: 'boolean',
+ description: 'Show memory usage in status bar',
+ default: false,
+ })
+ .deprecateOption(
+ 'show_memory_usage',
+ 'Use --show-memory-usage instead. We will be removing --show_memory_usage in the coming weeks.',
+ )
+ .option('yolo', {
+ alias: 'y',
+ type: 'boolean',
+ description:
+ 'Automatically accept all actions (aka YOLO mode, see https://www.youtube.com/watch?v=xvFZjo5PgG0 for more details)?',
+ default: false,
+ })
+ .option('telemetry', {
+ type: 'boolean',
+ description:
+ 'Enable telemetry? This flag specifically controls if telemetry is sent. Other --telemetry-* flags set specific values but do not enable telemetry on their own.',
+ })
+ .option('telemetry-target', {
+ type: 'string',
+ choices: ['local', 'gcp'],
+ description:
+ 'Set the telemetry target (local or gcp). Overrides settings files.',
+ })
+ .option('telemetry-otlp-endpoint', {
+ type: 'string',
+ description:
+ 'Set the OTLP endpoint for telemetry. Overrides environment variables and settings files.',
+ })
+ .option('telemetry-log-prompts', {
+ type: 'boolean',
+ description:
+ 'Enable or disable logging of user prompts for telemetry. Overrides settings files.',
+ })
+ .option('checkpointing', {
+ alias: 'c',
+ type: 'boolean',
+ description: 'Enables checkpointing of file edits',
+ default: false,
+ })
+ .option('allowed-mcp-server-names', {
+ type: 'array',
+ string: true,
+ description: 'Allowed MCP server names',
+ })
+ .option('extensions', {
+ alias: 'e',
+ type: 'array',
+ string: true,
+ description:
+ 'A list of extensions to use. If not provided, all extensions are used.',
+ })
+ .option('list-extensions', {
+ alias: 'l',
+ type: 'boolean',
+ description: 'List all available extensions and exit.',
+ })
+ .option('ide-mode', {
+ type: 'boolean',
+ description: 'Run in IDE mode?',
+ })
+ .option('openai-logging', {
+ type: 'boolean',
+ description:
+ 'Enable logging of OpenAI API calls for debugging and analysis',
+ })
+ .option('openai-api-key', {
+ type: 'string',
+ description: 'OpenAI API key to use for authentication',
+ })
+ .option('openai-base-url', {
+ type: 'string',
+ description: 'OpenAI base URL (for custom endpoints)',
+ })
+
+ .version(await getCliVersion()) // This will enable the --version flag based on package.json
+ .alias('v', 'version')
+ .help()
+ .alias('h', 'help')
+ .strict()
+ .check((argv) => {
+ if (argv.prompt && argv.promptInteractive) {
+ throw new Error(
+ 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together',
+ );
+ }
+ return true;
+ });
+
+ yargsInstance.wrap(yargsInstance.terminalWidth());
+ return yargsInstance.argv;
+}
+
+// This function is now a thin wrapper around the server's implementation.
+// It's kept in the CLI for now as App.tsx directly calls it for memory refresh.
+// TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself.
+export async function loadHierarchicalGeminiMemory(
+ currentWorkingDirectory: string,
+ debugMode: boolean,
+ fileService: FileDiscoveryService,
+ extensionContextFilePaths: string[] = [],
+): Promise<{ memoryContent: string; fileCount: number }> {
+ if (debugMode) {
+ logger.debug(
+ `CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory}`,
+ );
+ }
+ // Directly call the server function.
+ // The server function will use its own homedir() for the global path.
+ return loadServerHierarchicalMemory(
+ currentWorkingDirectory,
+ debugMode,
+ fileService,
+ extensionContextFilePaths,
+ );
+}
+
+export async function loadCliConfig(
+ settings: Settings,
+ extensions: Extension[],
+ sessionId: string,
+ argv: CliArgs,
+): Promise {
+ const debugMode =
+ argv.debug ||
+ [process.env.DEBUG, process.env.DEBUG_MODE].some(
+ (v) => v === 'true' || v === '1',
+ );
+
+ const ideMode =
+ (argv.ideMode ?? settings.ideMode ?? false) &&
+ process.env.TERM_PROGRAM === 'vscode' &&
+ !process.env.SANDBOX;
+
+ const activeExtensions = filterActiveExtensions(
+ extensions,
+ argv.extensions || [],
+ );
+
+ // Handle OpenAI API key from command line
+ if (argv.openaiApiKey) {
+ process.env.OPENAI_API_KEY = argv.openaiApiKey;
+ }
+
+ // Handle OpenAI base URL from command line
+ if (argv.openaiBaseUrl) {
+ process.env.OPENAI_BASE_URL = argv.openaiBaseUrl;
+ }
+
+ // Set the context filename in the server's memoryTool module BEFORE loading memory
+ // TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
+ // directly to the Config constructor in core, and have core handle setGeminiMdFilename.
+ // However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
+ if (settings.contextFileName) {
+ setServerGeminiMdFilename(settings.contextFileName);
+ } else {
+ // Reset to default if not provided in settings.
+ setServerGeminiMdFilename(getCurrentGeminiMdFilename());
+ }
+
+ const extensionContextFilePaths = activeExtensions.flatMap(
+ (e) => e.contextFiles,
+ );
+
+ const fileService = new FileDiscoveryService(process.cwd());
+ // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
+ const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
+ process.cwd(),
+ debugMode,
+ fileService,
+ extensionContextFilePaths,
+ );
+
+ let mcpServers = mergeMcpServers(settings, activeExtensions);
+ const excludeTools = mergeExcludeTools(settings, activeExtensions);
+
+ if (argv.allowedMcpServerNames) {
+ const allowedNames = new Set(argv.allowedMcpServerNames.filter(Boolean));
+ if (allowedNames.size > 0) {
+ mcpServers = Object.fromEntries(
+ Object.entries(mcpServers).filter(([key]) => allowedNames.has(key)),
+ );
+ } else {
+ mcpServers = {};
+ }
+ }
+
+ if (ideMode) {
+ mcpServers['_ide_server'] = new MCPServerConfig(
+ undefined, // command
+ undefined, // args
+ undefined, // env
+ undefined, // cwd
+ undefined, // url
+ 'http://localhost:3000/mcp', // httpUrl
+ undefined, // headers
+ undefined, // tcp
+ undefined, // timeout
+ false, // trust
+ 'IDE connection', // description
+ undefined, // includeTools
+ undefined, // excludeTools
+ );
+ }
+
+ const sandboxConfig = await loadSandboxConfig(settings, argv);
+
+ return new Config({
+ sessionId,
+ embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
+ sandbox: sandboxConfig,
+ targetDir: process.cwd(),
+ debugMode,
+ question: argv.promptInteractive || argv.prompt || '',
+ fullContext: argv.allFiles || argv.all_files || false,
+ coreTools: settings.coreTools || undefined,
+ excludeTools,
+ toolDiscoveryCommand: settings.toolDiscoveryCommand,
+ toolCallCommand: settings.toolCallCommand,
+ mcpServerCommand: settings.mcpServerCommand,
+ mcpServers,
+ userMemory: memoryContent,
+ geminiMdFileCount: fileCount,
+ approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT,
+ showMemoryUsage:
+ argv.showMemoryUsage ||
+ argv.show_memory_usage ||
+ settings.showMemoryUsage ||
+ false,
+ accessibility: settings.accessibility,
+ telemetry: {
+ enabled: argv.telemetry ?? settings.telemetry?.enabled,
+ target: (argv.telemetryTarget ??
+ settings.telemetry?.target) as TelemetryTarget,
+ otlpEndpoint:
+ argv.telemetryOtlpEndpoint ??
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
+ settings.telemetry?.otlpEndpoint,
+ logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
+ },
+ usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true,
+ // Git-aware file filtering settings
+ fileFiltering: {
+ respectGitIgnore: settings.fileFiltering?.respectGitIgnore,
+ enableRecursiveFileSearch:
+ settings.fileFiltering?.enableRecursiveFileSearch,
+ },
+ checkpointing: argv.checkpointing || settings.checkpointing?.enabled,
+ proxy:
+ process.env.HTTPS_PROXY ||
+ process.env.https_proxy ||
+ process.env.HTTP_PROXY ||
+ process.env.http_proxy,
+ cwd: process.cwd(),
+ fileDiscoveryService: fileService,
+ bugCommand: settings.bugCommand,
+ model: argv.model!,
+ extensionContextFilePaths,
+ maxSessionTurns: settings.maxSessionTurns ?? -1,
+ listExtensions: argv.listExtensions || false,
+ activeExtensions: activeExtensions.map((e) => ({
+ name: e.config.name,
+ version: e.config.version,
+ })),
+ noBrowser: !!process.env.NO_BROWSER,
+ ideMode,
+ enableOpenAILogging:
+ (typeof argv.openaiLogging === 'undefined'
+ ? settings.enableOpenAILogging
+ : argv.openaiLogging) ?? false,
+ sampling_params: settings.sampling_params,
+ });
+}
+
+function mergeMcpServers(settings: Settings, extensions: Extension[]) {
+ const mcpServers = { ...(settings.mcpServers || {}) };
+ for (const extension of extensions) {
+ Object.entries(extension.config.mcpServers || {}).forEach(
+ ([key, server]) => {
+ if (mcpServers[key]) {
+ logger.warn(
+ `Skipping extension MCP config for server with key "${key}" as it already exists.`,
+ );
+ return;
+ }
+ mcpServers[key] = server;
+ },
+ );
+ }
+ return mcpServers;
+}
+
+function mergeExcludeTools(
+ settings: Settings,
+ extensions: Extension[],
+): string[] {
+ const allExcludeTools = new Set(settings.excludeTools || []);
+ for (const extension of extensions) {
+ for (const tool of extension.config.excludeTools || []) {
+ allExcludeTools.add(tool);
+ }
+ }
+ return [...allExcludeTools];
+}
diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts
new file mode 100644
index 00000000..690dd312
--- /dev/null
+++ b/packages/cli/src/config/extension.test.ts
@@ -0,0 +1,151 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi } from 'vitest';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import {
+ EXTENSIONS_CONFIG_FILENAME,
+ EXTENSIONS_DIRECTORY_NAME,
+ filterActiveExtensions,
+ loadExtensions,
+} from './extension.js';
+
+vi.mock('os', async (importOriginal) => {
+ const os = await importOriginal();
+ return {
+ ...os,
+ homedir: vi.fn(),
+ };
+});
+
+describe('loadExtensions', () => {
+ let tempWorkspaceDir: string;
+ let tempHomeDir: string;
+
+ beforeEach(() => {
+ tempWorkspaceDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'gemini-cli-test-workspace-'),
+ );
+ tempHomeDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'gemini-cli-test-home-'),
+ );
+ vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
+ });
+
+ afterEach(() => {
+ fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
+ fs.rmSync(tempHomeDir, { recursive: true, force: true });
+ });
+
+ it('should load context file path when GEMINI.md is present', () => {
+ const workspaceExtensionsDir = path.join(
+ tempWorkspaceDir,
+ EXTENSIONS_DIRECTORY_NAME,
+ );
+ fs.mkdirSync(workspaceExtensionsDir, { recursive: true });
+ createExtension(workspaceExtensionsDir, 'ext1', '1.0.0', true);
+ createExtension(workspaceExtensionsDir, 'ext2', '2.0.0');
+
+ const extensions = loadExtensions(tempWorkspaceDir);
+
+ expect(extensions).toHaveLength(2);
+ const ext1 = extensions.find((e) => e.config.name === 'ext1');
+ const ext2 = extensions.find((e) => e.config.name === 'ext2');
+ expect(ext1?.contextFiles).toEqual([
+ path.join(workspaceExtensionsDir, 'ext1', 'QWEN.md'),
+ ]);
+ expect(ext2?.contextFiles).toEqual([]);
+ });
+
+ it('should load context file path from the extension config', () => {
+ const workspaceExtensionsDir = path.join(
+ tempWorkspaceDir,
+ EXTENSIONS_DIRECTORY_NAME,
+ );
+ fs.mkdirSync(workspaceExtensionsDir, { recursive: true });
+ createExtension(
+ workspaceExtensionsDir,
+ 'ext1',
+ '1.0.0',
+ false,
+ 'my-context-file.md',
+ );
+
+ const extensions = loadExtensions(tempWorkspaceDir);
+
+ expect(extensions).toHaveLength(1);
+ const ext1 = extensions.find((e) => e.config.name === 'ext1');
+ expect(ext1?.contextFiles).toEqual([
+ path.join(workspaceExtensionsDir, 'ext1', 'my-context-file.md'),
+ ]);
+ });
+});
+
+describe('filterActiveExtensions', () => {
+ const extensions = [
+ { config: { name: 'ext1', version: '1.0.0' }, contextFiles: [] },
+ { config: { name: 'ext2', version: '1.0.0' }, contextFiles: [] },
+ { config: { name: 'ext3', version: '1.0.0' }, contextFiles: [] },
+ ];
+
+ it('should return all extensions if no enabled extensions are provided', () => {
+ const activeExtensions = filterActiveExtensions(extensions, []);
+ expect(activeExtensions).toHaveLength(3);
+ });
+
+ it('should return only the enabled extensions', () => {
+ const activeExtensions = filterActiveExtensions(extensions, [
+ 'ext1',
+ 'ext3',
+ ]);
+ expect(activeExtensions).toHaveLength(2);
+ expect(activeExtensions.some((e) => e.config.name === 'ext1')).toBe(true);
+ expect(activeExtensions.some((e) => e.config.name === 'ext3')).toBe(true);
+ });
+
+ it('should return no extensions when "none" is provided', () => {
+ const activeExtensions = filterActiveExtensions(extensions, ['none']);
+ expect(activeExtensions).toHaveLength(0);
+ });
+
+ it('should handle case-insensitivity', () => {
+ const activeExtensions = filterActiveExtensions(extensions, ['EXT1']);
+ expect(activeExtensions).toHaveLength(1);
+ expect(activeExtensions[0].config.name).toBe('ext1');
+ });
+
+ it('should log an error for unknown extensions', () => {
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+ filterActiveExtensions(extensions, ['ext4']);
+ expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4');
+ consoleSpy.mockRestore();
+ });
+});
+
+function createExtension(
+ extensionsDir: string,
+ name: string,
+ version: string,
+ addContextFile = false,
+ contextFileName?: string,
+): void {
+ const extDir = path.join(extensionsDir, name);
+ fs.mkdirSync(extDir);
+ fs.writeFileSync(
+ path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
+ JSON.stringify({ name, version, contextFileName }),
+ );
+
+ if (addContextFile) {
+ fs.writeFileSync(path.join(extDir, 'QWEN.md'), 'context');
+ }
+
+ if (contextFileName) {
+ fs.writeFileSync(path.join(extDir, contextFileName), 'context');
+ }
+}
diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts
new file mode 100644
index 00000000..b6c3cb6e
--- /dev/null
+++ b/packages/cli/src/config/extension.ts
@@ -0,0 +1,159 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { MCPServerConfig } from '@qwen/qwen-code-core';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+
+export const EXTENSIONS_DIRECTORY_NAME = path.join('.qwen', 'extensions');
+export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
+
+export interface Extension {
+ config: ExtensionConfig;
+ contextFiles: string[];
+}
+
+export interface ExtensionConfig {
+ name: string;
+ version: string;
+ mcpServers?: Record;
+ contextFileName?: string | string[];
+ excludeTools?: string[];
+}
+
+export function loadExtensions(workspaceDir: string): Extension[] {
+ const allExtensions = [
+ ...loadExtensionsFromDir(workspaceDir),
+ ...loadExtensionsFromDir(os.homedir()),
+ ];
+
+ const uniqueExtensions = new Map();
+ for (const extension of allExtensions) {
+ if (!uniqueExtensions.has(extension.config.name)) {
+ console.log(
+ `Loading extension: ${extension.config.name} (version: ${extension.config.version})`,
+ );
+ uniqueExtensions.set(extension.config.name, extension);
+ }
+ }
+
+ return Array.from(uniqueExtensions.values());
+}
+
+function loadExtensionsFromDir(dir: string): Extension[] {
+ const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME);
+ if (!fs.existsSync(extensionsDir)) {
+ return [];
+ }
+
+ const extensions: Extension[] = [];
+ for (const subdir of fs.readdirSync(extensionsDir)) {
+ const extensionDir = path.join(extensionsDir, subdir);
+
+ const extension = loadExtension(extensionDir);
+ if (extension != null) {
+ extensions.push(extension);
+ }
+ }
+ return extensions;
+}
+
+function loadExtension(extensionDir: string): Extension | null {
+ if (!fs.statSync(extensionDir).isDirectory()) {
+ console.error(
+ `Warning: unexpected file ${extensionDir} in extensions directory.`,
+ );
+ return null;
+ }
+
+ const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
+ if (!fs.existsSync(configFilePath)) {
+ console.error(
+ `Warning: extension directory ${extensionDir} does not contain a config file ${configFilePath}.`,
+ );
+ return null;
+ }
+
+ try {
+ const configContent = fs.readFileSync(configFilePath, 'utf-8');
+ const config = JSON.parse(configContent) as ExtensionConfig;
+ if (!config.name || !config.version) {
+ console.error(
+ `Invalid extension config in ${configFilePath}: missing name or version.`,
+ );
+ return null;
+ }
+
+ const contextFiles = getContextFileNames(config)
+ .map((contextFileName) => path.join(extensionDir, contextFileName))
+ .filter((contextFilePath) => fs.existsSync(contextFilePath));
+
+ return {
+ config,
+ contextFiles,
+ };
+ } catch (e) {
+ console.error(
+ `Warning: error parsing extension config in ${configFilePath}: ${e}`,
+ );
+ return null;
+ }
+}
+
+function getContextFileNames(config: ExtensionConfig): string[] {
+ if (!config.contextFileName) {
+ return ['QWEN.md'];
+ } else if (!Array.isArray(config.contextFileName)) {
+ return [config.contextFileName];
+ }
+ return config.contextFileName;
+}
+
+export function filterActiveExtensions(
+ extensions: Extension[],
+ enabledExtensionNames: string[],
+): Extension[] {
+ if (enabledExtensionNames.length === 0) {
+ return extensions;
+ }
+
+ const lowerCaseEnabledExtensions = new Set(
+ enabledExtensionNames.map((e) => e.trim().toLowerCase()),
+ );
+
+ if (
+ lowerCaseEnabledExtensions.size === 1 &&
+ lowerCaseEnabledExtensions.has('none')
+ ) {
+ if (extensions.length > 0) {
+ console.log('All extensions are disabled.');
+ }
+ return [];
+ }
+
+ const activeExtensions: Extension[] = [];
+ const notFoundNames = new Set(lowerCaseEnabledExtensions);
+
+ for (const extension of extensions) {
+ const lowerCaseName = extension.config.name.toLowerCase();
+ if (lowerCaseEnabledExtensions.has(lowerCaseName)) {
+ console.log(
+ `Activated extension: ${extension.config.name} (version: ${extension.config.version})`,
+ );
+ activeExtensions.push(extension);
+ notFoundNames.delete(lowerCaseName);
+ } else {
+ console.log(`Disabled extension: ${extension.config.name}`);
+ }
+ }
+
+ for (const requestedName of notFoundNames) {
+ console.log(`Extension not found: ${requestedName}`);
+ }
+
+ return activeExtensions;
+}
diff --git a/packages/cli/src/config/sandboxConfig.ts b/packages/cli/src/config/sandboxConfig.ts
new file mode 100644
index 00000000..3d73bdb3
--- /dev/null
+++ b/packages/cli/src/config/sandboxConfig.ts
@@ -0,0 +1,107 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SandboxConfig } from '@qwen/qwen-code-core';
+import commandExists from 'command-exists';
+import * as os from 'node:os';
+import { getPackageJson } from '../utils/package.js';
+import { Settings } from './settings.js';
+
+// This is a stripped-down version of the CliArgs interface from config.ts
+// to avoid circular dependencies.
+interface SandboxCliArgs {
+ sandbox?: boolean | string;
+ sandboxImage?: string;
+}
+
+const VALID_SANDBOX_COMMANDS: ReadonlyArray = [
+ 'docker',
+ 'podman',
+ 'sandbox-exec',
+];
+
+function isSandboxCommand(value: string): value is SandboxConfig['command'] {
+ return (VALID_SANDBOX_COMMANDS as readonly string[]).includes(value);
+}
+
+function getSandboxCommand(
+ sandbox?: boolean | string,
+): SandboxConfig['command'] | '' {
+ // If the SANDBOX env var is set, we're already inside the sandbox.
+ if (process.env.SANDBOX) {
+ return '';
+ }
+
+ // note environment variable takes precedence over argument (from command line or settings)
+ const environmentConfiguredSandbox =
+ process.env.GEMINI_SANDBOX?.toLowerCase().trim() ?? '';
+ sandbox =
+ environmentConfiguredSandbox?.length > 0
+ ? environmentConfiguredSandbox
+ : sandbox;
+ if (sandbox === '1' || sandbox === 'true') sandbox = true;
+ else if (sandbox === '0' || sandbox === 'false' || !sandbox) sandbox = false;
+
+ if (sandbox === false) {
+ return '';
+ }
+
+ if (typeof sandbox === 'string' && sandbox) {
+ if (!isSandboxCommand(sandbox)) {
+ console.error(
+ `ERROR: invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join(
+ ', ',
+ )}`,
+ );
+ process.exit(1);
+ }
+ // confirm that specified command exists
+ if (commandExists.sync(sandbox)) {
+ return sandbox;
+ }
+ console.error(
+ `ERROR: missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
+ );
+ process.exit(1);
+ }
+
+ // look for seatbelt, docker, or podman, in that order
+ // for container-based sandboxing, require sandbox to be enabled explicitly
+ if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) {
+ return 'sandbox-exec';
+ } else if (commandExists.sync('docker') && sandbox === true) {
+ return 'docker';
+ } else if (commandExists.sync('podman') && sandbox === true) {
+ return 'podman';
+ }
+
+ // throw an error if user requested sandbox but no command was found
+ if (sandbox === true) {
+ console.error(
+ 'ERROR: GEMINI_SANDBOX is true but failed to determine command for sandbox; ' +
+ 'install docker or podman or specify command in GEMINI_SANDBOX',
+ );
+ process.exit(1);
+ }
+
+ return '';
+}
+
+export async function loadSandboxConfig(
+ settings: Settings,
+ argv: SandboxCliArgs,
+): Promise {
+ const sandboxOption = argv.sandbox ?? settings.sandbox;
+ const command = getSandboxCommand(sandboxOption);
+
+ const packageJson = await getPackageJson();
+ const image =
+ argv.sandboxImage ??
+ process.env.GEMINI_SANDBOX_IMAGE ??
+ packageJson?.config?.sandboxImageUri;
+
+ return command && image ? { command, image } : undefined;
+}
diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts
new file mode 100644
index 00000000..44de24fe
--- /dev/null
+++ b/packages/cli/src/config/settings.test.ts
@@ -0,0 +1,795 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+///
+
+// Mock 'os' first.
+import * as osActual from 'os'; // Import for type info for the mock factory
+vi.mock('os', async (importOriginal) => {
+ const actualOs = await importOriginal();
+ return {
+ ...actualOs,
+ homedir: vi.fn(() => '/mock/home/user'),
+ platform: vi.fn(() => 'linux'),
+ };
+});
+
+// Mock './settings.js' to ensure it uses the mocked 'os.homedir()' for its internal constants.
+vi.mock('./settings.js', async (importActual) => {
+ const originalModule = await importActual();
+ return {
+ __esModule: true, // Ensure correct module shape
+ ...originalModule, // Re-export all original members
+ // We are relying on originalModule's USER_SETTINGS_PATH being constructed with mocked os.homedir()
+ };
+});
+
+// NOW import everything else, including the (now effectively re-exported) settings.js
+import * as pathActual from 'path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH
+import {
+ describe,
+ it,
+ expect,
+ vi,
+ beforeEach,
+ afterEach,
+ type Mocked,
+ type Mock,
+} from 'vitest';
+import * as fs from 'fs'; // fs will be mocked separately
+import stripJsonComments from 'strip-json-comments'; // Will be mocked separately
+
+// These imports will get the versions from the vi.mock('./settings.js', ...) factory.
+import {
+ loadSettings,
+ USER_SETTINGS_PATH, // This IS the mocked path.
+ SYSTEM_SETTINGS_PATH,
+ SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock.
+ SettingScope,
+} from './settings.js';
+
+const MOCK_WORKSPACE_DIR = '/mock/workspace';
+// Use the (mocked) SETTINGS_DIRECTORY_NAME for consistency
+const MOCK_WORKSPACE_SETTINGS_PATH = pathActual.join(
+ MOCK_WORKSPACE_DIR,
+ SETTINGS_DIRECTORY_NAME,
+ 'settings.json',
+);
+
+vi.mock('fs');
+vi.mock('strip-json-comments', () => ({
+ default: vi.fn((content) => content),
+}));
+
+describe('Settings Loading and Merging', () => {
+ let mockFsExistsSync: Mocked;
+ let mockStripJsonComments: Mocked;
+ let mockFsMkdirSync: Mocked;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+
+ mockFsExistsSync = vi.mocked(fs.existsSync);
+ mockFsMkdirSync = vi.mocked(fs.mkdirSync);
+ mockStripJsonComments = vi.mocked(stripJsonComments);
+
+ vi.mocked(osActual.homedir).mockReturnValue('/mock/home/user');
+ (mockStripJsonComments as unknown as Mock).mockImplementation(
+ (jsonString: string) => jsonString,
+ );
+ (mockFsExistsSync as Mock).mockReturnValue(false);
+ (fs.readFileSync as Mock).mockReturnValue('{}'); // Return valid empty JSON
+ (mockFsMkdirSync as Mock).mockImplementation(() => undefined);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('loadSettings', () => {
+ it('should load empty settings if no files exist', () => {
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.system.settings).toEqual({});
+ expect(settings.user.settings).toEqual({});
+ expect(settings.workspace.settings).toEqual({});
+ expect(settings.merged).toEqual({});
+ expect(settings.errors.length).toBe(0);
+ });
+
+ it('should load system settings if only system file exists', () => {
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === SYSTEM_SETTINGS_PATH,
+ );
+ const systemSettingsContent = {
+ theme: 'system-default',
+ sandbox: false,
+ };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === SYSTEM_SETTINGS_PATH)
+ return JSON.stringify(systemSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(fs.readFileSync).toHaveBeenCalledWith(
+ SYSTEM_SETTINGS_PATH,
+ 'utf-8',
+ );
+ expect(settings.system.settings).toEqual(systemSettingsContent);
+ expect(settings.user.settings).toEqual({});
+ expect(settings.workspace.settings).toEqual({});
+ expect(settings.merged).toEqual(systemSettingsContent);
+ });
+
+ it('should load user settings if only user file exists', () => {
+ const expectedUserSettingsPath = USER_SETTINGS_PATH; // Use the path actually resolved by the (mocked) module
+
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === expectedUserSettingsPath,
+ );
+ const userSettingsContent = {
+ theme: 'dark',
+ contextFileName: 'USER_CONTEXT.md',
+ };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === expectedUserSettingsPath)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(fs.readFileSync).toHaveBeenCalledWith(
+ expectedUserSettingsPath,
+ 'utf-8',
+ );
+ expect(settings.user.settings).toEqual(userSettingsContent);
+ expect(settings.workspace.settings).toEqual({});
+ expect(settings.merged).toEqual(userSettingsContent);
+ });
+
+ it('should load workspace settings if only workspace file exists', () => {
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
+ );
+ const workspaceSettingsContent = {
+ sandbox: true,
+ contextFileName: 'WORKSPACE_CONTEXT.md',
+ };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(fs.readFileSync).toHaveBeenCalledWith(
+ MOCK_WORKSPACE_SETTINGS_PATH,
+ 'utf-8',
+ );
+ expect(settings.user.settings).toEqual({});
+ expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
+ expect(settings.merged).toEqual(workspaceSettingsContent);
+ });
+
+ it('should merge user and workspace settings, with workspace taking precedence', () => {
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const userSettingsContent = {
+ theme: 'dark',
+ sandbox: false,
+ contextFileName: 'USER_CONTEXT.md',
+ };
+ const workspaceSettingsContent = {
+ sandbox: true,
+ coreTools: ['tool1'],
+ contextFileName: 'WORKSPACE_CONTEXT.md',
+ };
+
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(settings.user.settings).toEqual(userSettingsContent);
+ expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
+ expect(settings.merged).toEqual({
+ theme: 'dark',
+ sandbox: true,
+ coreTools: ['tool1'],
+ contextFileName: 'WORKSPACE_CONTEXT.md',
+ });
+ });
+
+ it('should merge system, user and workspace settings, with system taking precedence over workspace, and workspace over user', () => {
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const systemSettingsContent = {
+ theme: 'system-theme',
+ sandbox: false,
+ telemetry: { enabled: false },
+ };
+ const userSettingsContent = {
+ theme: 'dark',
+ sandbox: true,
+ contextFileName: 'USER_CONTEXT.md',
+ };
+ const workspaceSettingsContent = {
+ sandbox: false,
+ coreTools: ['tool1'],
+ contextFileName: 'WORKSPACE_CONTEXT.md',
+ };
+
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === SYSTEM_SETTINGS_PATH)
+ return JSON.stringify(systemSettingsContent);
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(settings.system.settings).toEqual(systemSettingsContent);
+ expect(settings.user.settings).toEqual(userSettingsContent);
+ expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
+ expect(settings.merged).toEqual({
+ theme: 'system-theme',
+ sandbox: false,
+ telemetry: { enabled: false },
+ coreTools: ['tool1'],
+ contextFileName: 'WORKSPACE_CONTEXT.md',
+ });
+ });
+
+ it('should handle contextFileName correctly when only in user settings', () => {
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ const userSettingsContent = { contextFileName: 'CUSTOM.md' };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.merged.contextFileName).toBe('CUSTOM.md');
+ });
+
+ it('should handle contextFileName correctly when only in workspace settings', () => {
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
+ );
+ const workspaceSettingsContent = {
+ contextFileName: 'PROJECT_SPECIFIC.md',
+ };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.merged.contextFileName).toBe('PROJECT_SPECIFIC.md');
+ });
+
+ it('should default contextFileName to undefined if not in any settings file', () => {
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const userSettingsContent = { theme: 'dark' };
+ const workspaceSettingsContent = { sandbox: true };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.merged.contextFileName).toBeUndefined();
+ });
+
+ it('should load telemetry setting from user settings', () => {
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ const userSettingsContent = { telemetry: true };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.merged.telemetry).toBe(true);
+ });
+
+ it('should load telemetry setting from workspace settings', () => {
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
+ );
+ const workspaceSettingsContent = { telemetry: false };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '{}';
+ },
+ );
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.merged.telemetry).toBe(false);
+ });
+
+ it('should prioritize workspace telemetry setting over user setting', () => {
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const userSettingsContent = { telemetry: true };
+ const workspaceSettingsContent = { telemetry: false };
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '{}';
+ },
+ );
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.merged.telemetry).toBe(false);
+ });
+
+ it('should have telemetry as undefined if not in any settings file', () => {
+ (mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist
+ (fs.readFileSync as Mock).mockReturnValue('{}');
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.merged.telemetry).toBeUndefined();
+ });
+
+ it('should handle JSON parsing errors gracefully', () => {
+ (mockFsExistsSync as Mock).mockReturnValue(true); // Both files "exist"
+ const invalidJsonContent = 'invalid json';
+ const userReadError = new SyntaxError(
+ "Expected ',' or '}' after property value in JSON at position 10",
+ );
+ const workspaceReadError = new SyntaxError(
+ 'Unexpected token i in JSON at position 0',
+ );
+
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH) {
+ // Simulate JSON.parse throwing for user settings
+ vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
+ throw userReadError;
+ });
+ return invalidJsonContent; // Content that would cause JSON.parse to throw
+ }
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
+ // Simulate JSON.parse throwing for workspace settings
+ vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
+ throw workspaceReadError;
+ });
+ return invalidJsonContent;
+ }
+ return '{}'; // Default for other reads
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ // Check that settings are empty due to parsing errors
+ expect(settings.user.settings).toEqual({});
+ expect(settings.workspace.settings).toEqual({});
+ expect(settings.merged).toEqual({});
+
+ // Check that error objects are populated in settings.errors
+ expect(settings.errors).toBeDefined();
+ // Assuming both user and workspace files cause errors and are added in order
+ expect(settings.errors.length).toEqual(2);
+
+ const userError = settings.errors.find(
+ (e) => e.path === USER_SETTINGS_PATH,
+ );
+ expect(userError).toBeDefined();
+ expect(userError?.message).toBe(userReadError.message);
+
+ const workspaceError = settings.errors.find(
+ (e) => e.path === MOCK_WORKSPACE_SETTINGS_PATH,
+ );
+ expect(workspaceError).toBeDefined();
+ expect(workspaceError?.message).toBe(workspaceReadError.message);
+
+ // Restore JSON.parse mock if it was spied on specifically for this test
+ vi.restoreAllMocks(); // Or more targeted restore if needed
+ });
+
+ it('should resolve environment variables in user settings', () => {
+ process.env.TEST_API_KEY = 'user_api_key_from_env';
+ const userSettingsContent = {
+ apiKey: '$TEST_API_KEY',
+ someUrl: 'https://test.com/${TEST_API_KEY}',
+ };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.apiKey).toBe('user_api_key_from_env');
+ expect(settings.user.settings.someUrl).toBe(
+ 'https://test.com/user_api_key_from_env',
+ );
+ expect(settings.merged.apiKey).toBe('user_api_key_from_env');
+ delete process.env.TEST_API_KEY;
+ });
+
+ it('should resolve environment variables in workspace settings', () => {
+ process.env.WORKSPACE_ENDPOINT = 'workspace_endpoint_from_env';
+ const workspaceSettingsContent = {
+ endpoint: '${WORKSPACE_ENDPOINT}/api',
+ nested: { value: '$WORKSPACE_ENDPOINT' },
+ };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.workspace.settings.endpoint).toBe(
+ 'workspace_endpoint_from_env/api',
+ );
+ expect(settings.workspace.settings.nested.value).toBe(
+ 'workspace_endpoint_from_env',
+ );
+ expect(settings.merged.endpoint).toBe('workspace_endpoint_from_env/api');
+ delete process.env.WORKSPACE_ENDPOINT;
+ });
+
+ it('should prioritize user env variables over workspace env variables if keys clash after resolution', () => {
+ const userSettingsContent = { configValue: '$SHARED_VAR' };
+ const workspaceSettingsContent = { configValue: '$SHARED_VAR' };
+
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const originalSharedVar = process.env.SHARED_VAR;
+ // Temporarily delete to ensure a clean slate for the test's specific manipulations
+ delete process.env.SHARED_VAR;
+
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH) {
+ process.env.SHARED_VAR = 'user_value_for_user_read'; // Set for user settings read
+ return JSON.stringify(userSettingsContent);
+ }
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
+ process.env.SHARED_VAR = 'workspace_value_for_workspace_read'; // Set for workspace settings read
+ return JSON.stringify(workspaceSettingsContent);
+ }
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(settings.user.settings.configValue).toBe(
+ 'user_value_for_user_read',
+ );
+ expect(settings.workspace.settings.configValue).toBe(
+ 'workspace_value_for_workspace_read',
+ );
+ // Merged should take workspace's resolved value
+ expect(settings.merged.configValue).toBe(
+ 'workspace_value_for_workspace_read',
+ );
+
+ // Restore original environment variable state
+ if (originalSharedVar !== undefined) {
+ process.env.SHARED_VAR = originalSharedVar;
+ } else {
+ delete process.env.SHARED_VAR; // Ensure it's deleted if it wasn't there before
+ }
+ });
+
+ it('should prioritize workspace env variables over user env variables if keys clash after resolution', () => {
+ const userSettingsContent = { configValue: '$SHARED_VAR' };
+ const workspaceSettingsContent = { configValue: '$SHARED_VAR' };
+
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const originalSharedVar = process.env.SHARED_VAR;
+ // Temporarily delete to ensure a clean slate for the test's specific manipulations
+ delete process.env.SHARED_VAR;
+
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH) {
+ process.env.SHARED_VAR = 'user_value_for_user_read'; // Set for user settings read
+ return JSON.stringify(userSettingsContent);
+ }
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
+ process.env.SHARED_VAR = 'workspace_value_for_workspace_read'; // Set for workspace settings read
+ return JSON.stringify(workspaceSettingsContent);
+ }
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(settings.user.settings.configValue).toBe(
+ 'user_value_for_user_read',
+ );
+ expect(settings.workspace.settings.configValue).toBe(
+ 'workspace_value_for_workspace_read',
+ );
+ // Merged should take workspace's resolved value
+ expect(settings.merged.configValue).toBe(
+ 'workspace_value_for_workspace_read',
+ );
+
+ // Restore original environment variable state
+ if (originalSharedVar !== undefined) {
+ process.env.SHARED_VAR = originalSharedVar;
+ } else {
+ delete process.env.SHARED_VAR; // Ensure it's deleted if it wasn't there before
+ }
+ });
+
+ it('should prioritize system env variables over workspace env variables if keys clash after resolution', () => {
+ const workspaceSettingsContent = { configValue: '$SHARED_VAR' };
+ const systemSettingsContent = { configValue: '$SHARED_VAR' };
+
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const originalSharedVar = process.env.SHARED_VAR;
+ // Temporarily delete to ensure a clean slate for the test's specific manipulations
+ delete process.env.SHARED_VAR;
+
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === SYSTEM_SETTINGS_PATH) {
+ process.env.SHARED_VAR = 'system_value_for_system_read'; // Set for system settings read
+ return JSON.stringify(systemSettingsContent);
+ }
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
+ process.env.SHARED_VAR = 'workspace_value_for_workspace_read'; // Set for workspace settings read
+ return JSON.stringify(workspaceSettingsContent);
+ }
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(settings.system.settings.configValue).toBe(
+ 'system_value_for_system_read',
+ );
+ expect(settings.workspace.settings.configValue).toBe(
+ 'workspace_value_for_workspace_read',
+ );
+ // Merged should take workspace's resolved value
+ expect(settings.merged.configValue).toBe('system_value_for_system_read');
+
+ // Restore original environment variable state
+ if (originalSharedVar !== undefined) {
+ process.env.SHARED_VAR = originalSharedVar;
+ } else {
+ delete process.env.SHARED_VAR; // Ensure it's deleted if it wasn't there before
+ }
+ });
+
+ it('should leave unresolved environment variables as is', () => {
+ const userSettingsContent = { apiKey: '$UNDEFINED_VAR' };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.apiKey).toBe('$UNDEFINED_VAR');
+ expect(settings.merged.apiKey).toBe('$UNDEFINED_VAR');
+ });
+
+ it('should resolve multiple environment variables in a single string', () => {
+ process.env.VAR_A = 'valueA';
+ process.env.VAR_B = 'valueB';
+ const userSettingsContent = { path: '/path/$VAR_A/${VAR_B}/end' };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.path).toBe('/path/valueA/valueB/end');
+ delete process.env.VAR_A;
+ delete process.env.VAR_B;
+ });
+
+ it('should resolve environment variables in arrays', () => {
+ process.env.ITEM_1 = 'item1_env';
+ process.env.ITEM_2 = 'item2_env';
+ const userSettingsContent = { list: ['$ITEM_1', '${ITEM_2}', 'literal'] };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.list).toEqual([
+ 'item1_env',
+ 'item2_env',
+ 'literal',
+ ]);
+ delete process.env.ITEM_1;
+ delete process.env.ITEM_2;
+ });
+
+ it('should correctly pass through null, boolean, and number types, and handle undefined properties', () => {
+ process.env.MY_ENV_STRING = 'env_string_value';
+ process.env.MY_ENV_STRING_NESTED = 'env_string_nested_value';
+
+ const userSettingsContent = {
+ nullVal: null,
+ trueVal: true,
+ falseVal: false,
+ numberVal: 123.45,
+ stringVal: '$MY_ENV_STRING',
+ nestedObj: {
+ nestedNull: null,
+ nestedBool: true,
+ nestedNum: 0,
+ nestedString: 'literal',
+ anotherEnv: '${MY_ENV_STRING_NESTED}',
+ },
+ };
+
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(settings.user.settings.nullVal).toBeNull();
+ expect(settings.user.settings.trueVal).toBe(true);
+ expect(settings.user.settings.falseVal).toBe(false);
+ expect(settings.user.settings.numberVal).toBe(123.45);
+ expect(settings.user.settings.stringVal).toBe('env_string_value');
+ expect(settings.user.settings.undefinedVal).toBeUndefined();
+
+ expect(settings.user.settings.nestedObj.nestedNull).toBeNull();
+ expect(settings.user.settings.nestedObj.nestedBool).toBe(true);
+ expect(settings.user.settings.nestedObj.nestedNum).toBe(0);
+ expect(settings.user.settings.nestedObj.nestedString).toBe('literal');
+ expect(settings.user.settings.nestedObj.anotherEnv).toBe(
+ 'env_string_nested_value',
+ );
+
+ delete process.env.MY_ENV_STRING;
+ delete process.env.MY_ENV_STRING_NESTED;
+ });
+
+ it('should resolve multiple concatenated environment variables in a single string value', () => {
+ process.env.TEST_HOST = 'myhost';
+ process.env.TEST_PORT = '9090';
+ const userSettingsContent = {
+ serverAddress: '${TEST_HOST}:${TEST_PORT}/api',
+ };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.serverAddress).toBe('myhost:9090/api');
+
+ delete process.env.TEST_HOST;
+ delete process.env.TEST_PORT;
+ });
+ });
+
+ describe('LoadedSettings class', () => {
+ it('setValue should update the correct scope and recompute merged settings', () => {
+ (mockFsExistsSync as Mock).mockReturnValue(false);
+ const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ vi.mocked(fs.writeFileSync).mockImplementation(() => {});
+ // mkdirSync is mocked in beforeEach to return undefined, which is fine for void usage
+
+ loadedSettings.setValue(SettingScope.User, 'theme', 'matrix');
+ expect(loadedSettings.user.settings.theme).toBe('matrix');
+ expect(loadedSettings.merged.theme).toBe('matrix');
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ USER_SETTINGS_PATH,
+ JSON.stringify({ theme: 'matrix' }, null, 2),
+ 'utf-8',
+ );
+
+ loadedSettings.setValue(
+ SettingScope.Workspace,
+ 'contextFileName',
+ 'MY_AGENTS.md',
+ );
+ expect(loadedSettings.workspace.settings.contextFileName).toBe(
+ 'MY_AGENTS.md',
+ );
+ expect(loadedSettings.merged.contextFileName).toBe('MY_AGENTS.md');
+ expect(loadedSettings.merged.theme).toBe('matrix'); // User setting should still be there
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ MOCK_WORKSPACE_SETTINGS_PATH,
+ JSON.stringify({ contextFileName: 'MY_AGENTS.md' }, null, 2),
+ 'utf-8',
+ );
+
+ // System theme overrides user and workspace themes
+ loadedSettings.setValue(SettingScope.System, 'theme', 'ocean');
+
+ expect(loadedSettings.system.settings.theme).toBe('ocean');
+ expect(loadedSettings.merged.theme).toBe('ocean');
+ });
+ });
+});
diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts
new file mode 100644
index 00000000..df86eae4
--- /dev/null
+++ b/packages/cli/src/config/settings.ts
@@ -0,0 +1,388 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import { homedir, platform } from 'os';
+import * as dotenv from 'dotenv';
+import {
+ MCPServerConfig,
+ GEMINI_CONFIG_DIR as GEMINI_DIR,
+ getErrorMessage,
+ BugCommandSettings,
+ TelemetrySettings,
+ AuthType,
+} from '@qwen/qwen-code-core';
+import stripJsonComments from 'strip-json-comments';
+import { DefaultLight } from '../ui/themes/default-light.js';
+import { DefaultDark } from '../ui/themes/default.js';
+
+export const SETTINGS_DIRECTORY_NAME = '.qwen';
+export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
+export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
+
+function getSystemSettingsPath(): string {
+ if (platform() === 'darwin') {
+ return '/Library/Application Support/QwenCode/settings.json';
+ } else if (platform() === 'win32') {
+ return 'C:\\ProgramData\\qwen-code\\settings.json';
+ } else {
+ return '/etc/qwen-code/settings.json';
+ }
+}
+
+export const SYSTEM_SETTINGS_PATH = getSystemSettingsPath();
+
+export enum SettingScope {
+ User = 'User',
+ Workspace = 'Workspace',
+ System = 'System',
+}
+
+export interface CheckpointingSettings {
+ enabled?: boolean;
+}
+
+export interface AccessibilitySettings {
+ disableLoadingPhrases?: boolean;
+}
+
+export interface Settings {
+ theme?: string;
+ selectedAuthType?: AuthType;
+ sandbox?: boolean | string;
+ coreTools?: string[];
+ excludeTools?: string[];
+ toolDiscoveryCommand?: string;
+ toolCallCommand?: string;
+ mcpServerCommand?: string;
+ mcpServers?: Record;
+ showMemoryUsage?: boolean;
+ contextFileName?: string | string[];
+ accessibility?: AccessibilitySettings;
+ telemetry?: TelemetrySettings;
+ usageStatisticsEnabled?: boolean;
+ preferredEditor?: string;
+ bugCommand?: BugCommandSettings;
+ checkpointing?: CheckpointingSettings;
+ autoConfigureMaxOldSpaceSize?: boolean;
+ enableOpenAILogging?: boolean;
+
+ // Git-aware file filtering settings
+ fileFiltering?: {
+ respectGitIgnore?: boolean;
+ enableRecursiveFileSearch?: boolean;
+ };
+
+ // UI setting. Does not display the ANSI-controlled terminal title.
+ hideWindowTitle?: boolean;
+ hideTips?: boolean;
+ hideBanner?: boolean;
+
+ // Setting for setting maximum number of user/model/tool turns in a session.
+ maxSessionTurns?: number;
+
+ // Sampling parameters for content generation
+ sampling_params?: {
+ top_p?: number;
+ top_k?: number;
+ repetition_penalty?: number;
+ presence_penalty?: number;
+ frequency_penalty?: number;
+ temperature?: number;
+ max_tokens?: number;
+ };
+
+ // Add other settings here.
+ ideMode?: boolean;
+}
+
+export interface SettingsError {
+ message: string;
+ path: string;
+}
+
+export interface SettingsFile {
+ settings: Settings;
+ path: string;
+}
+export class LoadedSettings {
+ constructor(
+ system: SettingsFile,
+ user: SettingsFile,
+ workspace: SettingsFile,
+ errors: SettingsError[],
+ ) {
+ this.system = system;
+ this.user = user;
+ this.workspace = workspace;
+ this.errors = errors;
+ this._merged = this.computeMergedSettings();
+ }
+
+ readonly system: SettingsFile;
+ readonly user: SettingsFile;
+ readonly workspace: SettingsFile;
+ readonly errors: SettingsError[];
+
+ private _merged: Settings;
+
+ get merged(): Settings {
+ return this._merged;
+ }
+
+ private computeMergedSettings(): Settings {
+ return {
+ ...this.user.settings,
+ ...this.workspace.settings,
+ ...this.system.settings,
+ };
+ }
+
+ forScope(scope: SettingScope): SettingsFile {
+ switch (scope) {
+ case SettingScope.User:
+ return this.user;
+ case SettingScope.Workspace:
+ return this.workspace;
+ case SettingScope.System:
+ return this.system;
+ default:
+ throw new Error(`Invalid scope: ${scope}`);
+ }
+ }
+
+ setValue(
+ scope: SettingScope,
+ key: keyof Settings,
+ value: string | Record | undefined,
+ ): void {
+ const settingsFile = this.forScope(scope);
+ // @ts-expect-error - value can be string | Record
+ settingsFile.settings[key] = value;
+ this._merged = this.computeMergedSettings();
+ saveSettings(settingsFile);
+ }
+}
+
+function resolveEnvVarsInString(value: string): string {
+ const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME}
+ return value.replace(envVarRegex, (match, varName1, varName2) => {
+ const varName = varName1 || varName2;
+ if (process && process.env && typeof process.env[varName] === 'string') {
+ return process.env[varName]!;
+ }
+ return match;
+ });
+}
+
+function resolveEnvVarsInObject(obj: T): T {
+ if (
+ obj === null ||
+ obj === undefined ||
+ typeof obj === 'boolean' ||
+ typeof obj === 'number'
+ ) {
+ return obj;
+ }
+
+ if (typeof obj === 'string') {
+ return resolveEnvVarsInString(obj) as unknown as T;
+ }
+
+ if (Array.isArray(obj)) {
+ return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T;
+ }
+
+ if (typeof obj === 'object') {
+ const newObj = { ...obj } as T;
+ for (const key in newObj) {
+ if (Object.prototype.hasOwnProperty.call(newObj, key)) {
+ newObj[key] = resolveEnvVarsInObject(newObj[key]);
+ }
+ }
+ return newObj;
+ }
+
+ return obj;
+}
+
+function findEnvFile(startDir: string): string | null {
+ let currentDir = path.resolve(startDir);
+ while (true) {
+ // prefer gemini-specific .env under GEMINI_DIR
+ const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
+ if (fs.existsSync(geminiEnvPath)) {
+ return geminiEnvPath;
+ }
+ const envPath = path.join(currentDir, '.env');
+ if (fs.existsSync(envPath)) {
+ return envPath;
+ }
+ const parentDir = path.dirname(currentDir);
+ if (parentDir === currentDir || !parentDir) {
+ // check .env under home as fallback, again preferring gemini-specific .env
+ const homeGeminiEnvPath = path.join(homedir(), GEMINI_DIR, '.env');
+ if (fs.existsSync(homeGeminiEnvPath)) {
+ return homeGeminiEnvPath;
+ }
+ const homeEnvPath = path.join(homedir(), '.env');
+ if (fs.existsSync(homeEnvPath)) {
+ return homeEnvPath;
+ }
+ return null;
+ }
+ currentDir = parentDir;
+ }
+}
+
+export function setUpCloudShellEnvironment(envFilePath: string | null): void {
+ // Special handling for GOOGLE_CLOUD_PROJECT in Cloud Shell:
+ // Because GOOGLE_CLOUD_PROJECT in Cloud Shell tracks the project
+ // set by the user using "gcloud config set project" we do not want to
+ // use its value. So, unless the user overrides GOOGLE_CLOUD_PROJECT in
+ // one of the .env files, we set the Cloud Shell-specific default here.
+ if (envFilePath && fs.existsSync(envFilePath)) {
+ const envFileContent = fs.readFileSync(envFilePath);
+ const parsedEnv = dotenv.parse(envFileContent);
+ if (parsedEnv.GOOGLE_CLOUD_PROJECT) {
+ // .env file takes precedence in Cloud Shell
+ process.env.GOOGLE_CLOUD_PROJECT = parsedEnv.GOOGLE_CLOUD_PROJECT;
+ } else {
+ // If not in .env, set to default and override global
+ process.env.GOOGLE_CLOUD_PROJECT = 'cloudshell-gca';
+ }
+ } else {
+ // If no .env file, set to default and override global
+ process.env.GOOGLE_CLOUD_PROJECT = 'cloudshell-gca';
+ }
+}
+
+export function loadEnvironment(): void {
+ const envFilePath = findEnvFile(process.cwd());
+
+ if (process.env.CLOUD_SHELL === 'true') {
+ setUpCloudShellEnvironment(envFilePath);
+ }
+
+ if (envFilePath) {
+ dotenv.config({ path: envFilePath, quiet: true });
+ }
+}
+
+/**
+ * Loads settings from user and workspace directories.
+ * Project settings override user settings.
+ */
+export function loadSettings(workspaceDir: string): LoadedSettings {
+ loadEnvironment();
+ let systemSettings: Settings = {};
+ let userSettings: Settings = {};
+ let workspaceSettings: Settings = {};
+ const settingsErrors: SettingsError[] = [];
+
+ // Load system settings
+ try {
+ if (fs.existsSync(SYSTEM_SETTINGS_PATH)) {
+ const systemContent = fs.readFileSync(SYSTEM_SETTINGS_PATH, 'utf-8');
+ const parsedSystemSettings = JSON.parse(
+ stripJsonComments(systemContent),
+ ) as Settings;
+ systemSettings = resolveEnvVarsInObject(parsedSystemSettings);
+ }
+ } catch (error: unknown) {
+ settingsErrors.push({
+ message: getErrorMessage(error),
+ path: SYSTEM_SETTINGS_PATH,
+ });
+ }
+
+ // Load user settings
+ try {
+ if (fs.existsSync(USER_SETTINGS_PATH)) {
+ const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
+ const parsedUserSettings = JSON.parse(
+ stripJsonComments(userContent),
+ ) as Settings;
+ userSettings = resolveEnvVarsInObject(parsedUserSettings);
+ // Support legacy theme names
+ if (userSettings.theme && userSettings.theme === 'VS') {
+ userSettings.theme = DefaultLight.name;
+ } else if (userSettings.theme && userSettings.theme === 'VS2015') {
+ userSettings.theme = DefaultDark.name;
+ }
+ }
+ } catch (error: unknown) {
+ settingsErrors.push({
+ message: getErrorMessage(error),
+ path: USER_SETTINGS_PATH,
+ });
+ }
+
+ const workspaceSettingsPath = path.join(
+ workspaceDir,
+ SETTINGS_DIRECTORY_NAME,
+ 'settings.json',
+ );
+
+ // Load workspace settings
+ try {
+ if (fs.existsSync(workspaceSettingsPath)) {
+ const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
+ const parsedWorkspaceSettings = JSON.parse(
+ stripJsonComments(projectContent),
+ ) as Settings;
+ workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
+ if (workspaceSettings.theme && workspaceSettings.theme === 'VS') {
+ workspaceSettings.theme = DefaultLight.name;
+ } else if (
+ workspaceSettings.theme &&
+ workspaceSettings.theme === 'VS2015'
+ ) {
+ workspaceSettings.theme = DefaultDark.name;
+ }
+ }
+ } catch (error: unknown) {
+ settingsErrors.push({
+ message: getErrorMessage(error),
+ path: workspaceSettingsPath,
+ });
+ }
+
+ return new LoadedSettings(
+ {
+ path: SYSTEM_SETTINGS_PATH,
+ settings: systemSettings,
+ },
+ {
+ path: USER_SETTINGS_PATH,
+ settings: userSettings,
+ },
+ {
+ path: workspaceSettingsPath,
+ settings: workspaceSettings,
+ },
+ settingsErrors,
+ );
+}
+
+export function saveSettings(settingsFile: SettingsFile): void {
+ try {
+ // Ensure the directory exists
+ const dirPath = path.dirname(settingsFile.path);
+ if (!fs.existsSync(dirPath)) {
+ fs.mkdirSync(dirPath, { recursive: true });
+ }
+
+ fs.writeFileSync(
+ settingsFile.path,
+ JSON.stringify(settingsFile.settings, null, 2),
+ 'utf-8',
+ );
+ } catch (error) {
+ console.error('Error saving user settings file:', error);
+ }
+}
diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx
new file mode 100644
index 00000000..ac803f52
--- /dev/null
+++ b/packages/cli/src/gemini.test.tsx
@@ -0,0 +1,151 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import stripAnsi from 'strip-ansi';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { main } from './gemini.js';
+import {
+ LoadedSettings,
+ SettingsFile,
+ loadSettings,
+} from './config/settings.js';
+
+// Custom error to identify mock process.exit calls
+class MockProcessExitError extends Error {
+ constructor(readonly code?: string | number | null | undefined) {
+ super('PROCESS_EXIT_MOCKED');
+ this.name = 'MockProcessExitError';
+ }
+}
+
+// Mock dependencies
+vi.mock('./config/settings.js', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ loadSettings: vi.fn(),
+ };
+});
+
+vi.mock('./config/config.js', () => ({
+ loadCliConfig: vi.fn().mockResolvedValue({
+ config: {
+ getSandbox: vi.fn(() => false),
+ getQuestion: vi.fn(() => ''),
+ },
+ modelWasSwitched: false,
+ originalModelBeforeSwitch: null,
+ finalModel: 'test-model',
+ }),
+}));
+
+vi.mock('read-package-up', () => ({
+ readPackageUp: vi.fn().mockResolvedValue({
+ packageJson: { name: 'test-pkg', version: 'test-version' },
+ path: '/fake/path/package.json',
+ }),
+}));
+
+vi.mock('update-notifier', () => ({
+ default: vi.fn(() => ({
+ notify: vi.fn(),
+ })),
+}));
+
+vi.mock('./utils/sandbox.js', () => ({
+ sandbox_command: vi.fn(() => ''), // Default to no sandbox command
+ start_sandbox: vi.fn(() => Promise.resolve()), // Mock as an async function that resolves
+}));
+
+describe('gemini.tsx main function', () => {
+ let consoleErrorSpy: ReturnType;
+ let loadSettingsMock: ReturnType>;
+ let originalEnvGeminiSandbox: string | undefined;
+ let originalEnvSandbox: string | undefined;
+
+ const processExitSpy = vi
+ .spyOn(process, 'exit')
+ .mockImplementation((code) => {
+ throw new MockProcessExitError(code);
+ });
+
+ beforeEach(() => {
+ loadSettingsMock = vi.mocked(loadSettings);
+
+ // Store and clear sandbox-related env variables to ensure a consistent test environment
+ originalEnvGeminiSandbox = process.env.GEMINI_SANDBOX;
+ originalEnvSandbox = process.env.SANDBOX;
+ delete process.env.GEMINI_SANDBOX;
+ delete process.env.SANDBOX;
+
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ // Restore original env variables
+ if (originalEnvGeminiSandbox !== undefined) {
+ process.env.GEMINI_SANDBOX = originalEnvGeminiSandbox;
+ } else {
+ delete process.env.GEMINI_SANDBOX;
+ }
+ if (originalEnvSandbox !== undefined) {
+ process.env.SANDBOX = originalEnvSandbox;
+ } else {
+ delete process.env.SANDBOX;
+ }
+ vi.restoreAllMocks();
+ });
+
+ it('should call process.exit(1) if settings have errors', async () => {
+ const settingsError = {
+ message: 'Test settings error',
+ path: '/test/settings.json',
+ };
+ const userSettingsFile: SettingsFile = {
+ path: '/user/settings.json',
+ settings: {},
+ };
+ const workspaceSettingsFile: SettingsFile = {
+ path: '/workspace/.qwen/settings.json',
+ settings: {},
+ };
+ const systemSettingsFile: SettingsFile = {
+ path: '/system/settings.json',
+ settings: {},
+ };
+ const mockLoadedSettings = new LoadedSettings(
+ systemSettingsFile,
+ userSettingsFile,
+ workspaceSettingsFile,
+ [settingsError],
+ );
+
+ loadSettingsMock.mockReturnValue(mockLoadedSettings);
+
+ try {
+ await main();
+ // If main completes without throwing, the test should fail because process.exit was expected
+ expect.fail('main function did not exit as expected');
+ } catch (error) {
+ expect(error).toBeInstanceOf(MockProcessExitError);
+ if (error instanceof MockProcessExitError) {
+ expect(error.code).toBe(1);
+ }
+ }
+
+ // Verify console.error was called with the error message
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
+ expect(stripAnsi(String(consoleErrorSpy.mock.calls[0][0]))).toBe(
+ 'Error in /test/settings.json: Test settings error',
+ );
+ expect(stripAnsi(String(consoleErrorSpy.mock.calls[1][0]))).toBe(
+ 'Please fix /test/settings.json and try again.',
+ );
+
+ // Verify process.exit was called (indirectly, via the thrown error)
+ expect(processExitSpy).toHaveBeenCalledWith(1);
+ });
+});
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
new file mode 100644
index 00000000..9dcd48dd
--- /dev/null
+++ b/packages/cli/src/gemini.tsx
@@ -0,0 +1,344 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { render } from 'ink';
+import { AppWrapper } from './ui/App.js';
+import { loadCliConfig, parseArguments, CliArgs } from './config/config.js';
+import { readStdin } from './utils/readStdin.js';
+import { basename } from 'node:path';
+import v8 from 'node:v8';
+import os from 'node:os';
+import { spawn } from 'node:child_process';
+import { start_sandbox } from './utils/sandbox.js';
+import {
+ LoadedSettings,
+ loadSettings,
+ USER_SETTINGS_PATH,
+ SettingScope,
+} from './config/settings.js';
+import { themeManager } from './ui/themes/theme-manager.js';
+import { getStartupWarnings } from './utils/startupWarnings.js';
+import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
+import { runNonInteractive } from './nonInteractiveCli.js';
+import { loadExtensions, Extension } from './config/extension.js';
+import { cleanupCheckpoints, registerCleanup } from './utils/cleanup.js';
+import { getCliVersion } from './utils/version.js';
+import {
+ ApprovalMode,
+ Config,
+ EditTool,
+ ShellTool,
+ WriteFileTool,
+ sessionId,
+ logUserPrompt,
+ AuthType,
+ getOauthClient,
+} from '@qwen/qwen-code-core';
+import { validateAuthMethod } from './config/auth.js';
+import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
+
+function getNodeMemoryArgs(config: Config): string[] {
+ const totalMemoryMB = os.totalmem() / (1024 * 1024);
+ const heapStats = v8.getHeapStatistics();
+ const currentMaxOldSpaceSizeMb = Math.floor(
+ heapStats.heap_size_limit / 1024 / 1024,
+ );
+
+ // Set target to 50% of total memory
+ const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);
+ if (config.getDebugMode()) {
+ console.debug(
+ `Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`,
+ );
+ }
+
+ if (process.env.GEMINI_CLI_NO_RELAUNCH) {
+ return [];
+ }
+
+ if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
+ if (config.getDebugMode()) {
+ console.debug(
+ `Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`,
+ );
+ }
+ return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`];
+ }
+
+ return [];
+}
+
+async function relaunchWithAdditionalArgs(additionalArgs: string[]) {
+ const nodeArgs = [...additionalArgs, ...process.argv.slice(1)];
+ const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' };
+
+ const child = spawn(process.execPath, nodeArgs, {
+ stdio: 'inherit',
+ env: newEnv,
+ });
+
+ await new Promise((resolve) => child.on('close', resolve));
+ process.exit(0);
+}
+
+export async function main() {
+ const workspaceRoot = process.cwd();
+ const settings = loadSettings(workspaceRoot);
+
+ await cleanupCheckpoints();
+ if (settings.errors.length > 0) {
+ for (const error of settings.errors) {
+ let errorMessage = `Error in ${error.path}: ${error.message}`;
+ if (!process.env.NO_COLOR) {
+ errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
+ }
+ console.error(errorMessage);
+ console.error(`Please fix ${error.path} and try again.`);
+ }
+ process.exit(1);
+ }
+
+ const argv = await parseArguments();
+ const extensions = loadExtensions(workspaceRoot);
+ const config = await loadCliConfig(
+ settings.merged,
+ extensions,
+ sessionId,
+ argv,
+ );
+
+ if (argv.promptInteractive && !process.stdin.isTTY) {
+ console.error(
+ 'Error: The --prompt-interactive flag is not supported when piping input from stdin.',
+ );
+ process.exit(1);
+ }
+
+ if (config.getListExtensions()) {
+ console.log('Installed extensions:');
+ for (const extension of extensions) {
+ console.log(`- ${extension.config.name}`);
+ }
+ process.exit(0);
+ }
+
+ // Set a default auth type if one isn't set.
+ if (!settings.merged.selectedAuthType) {
+ if (process.env.CLOUD_SHELL === 'true') {
+ settings.setValue(
+ SettingScope.User,
+ 'selectedAuthType',
+ AuthType.CLOUD_SHELL,
+ );
+ }
+ }
+
+ setMaxSizedBoxDebugging(config.getDebugMode());
+
+ await config.initialize();
+
+ if (settings.merged.theme) {
+ if (!themeManager.setActiveTheme(settings.merged.theme)) {
+ // If the theme is not found during initial load, log a warning and continue.
+ // The useThemeCommand hook in App.tsx will handle opening the dialog.
+ console.warn(`Warning: Theme "${settings.merged.theme}" not found.`);
+ }
+ }
+
+ // hop into sandbox if we are outside and sandboxing is enabled
+ if (!process.env.SANDBOX) {
+ const memoryArgs = settings.merged.autoConfigureMaxOldSpaceSize
+ ? getNodeMemoryArgs(config)
+ : [];
+ const sandboxConfig = config.getSandbox();
+ if (sandboxConfig) {
+ if (settings.merged.selectedAuthType) {
+ // Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.
+ try {
+ const err = validateAuthMethod(settings.merged.selectedAuthType);
+ if (err) {
+ throw new Error(err);
+ }
+ await config.refreshAuth(settings.merged.selectedAuthType);
+ } catch (err) {
+ console.error('Error authenticating:', err);
+ process.exit(1);
+ }
+ }
+ await start_sandbox(sandboxConfig, memoryArgs);
+ process.exit(0);
+ } else {
+ // Not in a sandbox and not entering one, so relaunch with additional
+ // arguments to control memory usage if needed.
+ if (memoryArgs.length > 0) {
+ await relaunchWithAdditionalArgs(memoryArgs);
+ process.exit(0);
+ }
+ }
+ }
+
+ if (
+ settings.merged.selectedAuthType === AuthType.LOGIN_WITH_GOOGLE &&
+ config.getNoBrowser()
+ ) {
+ // Do oauth before app renders to make copying the link possible.
+ await getOauthClient(settings.merged.selectedAuthType, config);
+ }
+
+ let input = config.getQuestion();
+ const startupWarnings = [
+ ...(await getStartupWarnings()),
+ ...(await getUserStartupWarnings(workspaceRoot)),
+ ];
+
+ const shouldBeInteractive =
+ !!argv.promptInteractive || (process.stdin.isTTY && input?.length === 0);
+
+ // Render UI, passing necessary config values. Check that there is no command line question.
+ if (shouldBeInteractive) {
+ const version = await getCliVersion();
+ setWindowTitle(basename(workspaceRoot), settings);
+ const instance = render(
+
+
+ ,
+ { exitOnCtrlC: false },
+ );
+
+ registerCleanup(() => instance.unmount());
+ return;
+ }
+ // If not a TTY, read from stdin
+ // This is for cases where the user pipes input directly into the command
+ if (!process.stdin.isTTY && !input) {
+ input += await readStdin();
+ }
+ if (!input) {
+ console.error('No input provided via stdin.');
+ process.exit(1);
+ }
+
+ const prompt_id = Math.random().toString(16).slice(2);
+ logUserPrompt(config, {
+ 'event.name': 'user_prompt',
+ 'event.timestamp': new Date().toISOString(),
+ prompt: input,
+ prompt_id,
+ auth_type: config.getContentGeneratorConfig()?.authType,
+ prompt_length: input.length,
+ });
+
+ // Non-interactive mode handled by runNonInteractive
+ const nonInteractiveConfig = await loadNonInteractiveConfig(
+ config,
+ extensions,
+ settings,
+ argv,
+ );
+
+ await runNonInteractive(nonInteractiveConfig, input, prompt_id);
+ process.exit(0);
+}
+
+function setWindowTitle(title: string, settings: LoadedSettings) {
+ if (!settings.merged.hideWindowTitle) {
+ const windowTitle = (process.env.CLI_TITLE || `Qwen - ${title}`).replace(
+ // eslint-disable-next-line no-control-regex
+ /[\x00-\x1F\x7F]/g,
+ '',
+ );
+ process.stdout.write(`\x1b]2;${windowTitle}\x07`);
+
+ process.on('exit', () => {
+ process.stdout.write(`\x1b]2;\x07`);
+ });
+ }
+}
+
+// --- Global Unhandled Rejection Handler ---
+process.on('unhandledRejection', (reason, _promise) => {
+ // Log other unexpected unhandled rejections as critical errors
+ console.error('=========================================');
+ console.error('CRITICAL: Unhandled Promise Rejection!');
+ console.error('=========================================');
+ console.error('Reason:', reason);
+ console.error('Stack trace may follow:');
+ if (!(reason instanceof Error)) {
+ console.error(reason);
+ }
+ // Exit for genuinely unhandled errors
+ process.exit(1);
+});
+
+async function loadNonInteractiveConfig(
+ config: Config,
+ extensions: Extension[],
+ settings: LoadedSettings,
+ argv: CliArgs,
+) {
+ let finalConfig = config;
+ if (config.getApprovalMode() !== ApprovalMode.YOLO) {
+ // Everything is not allowed, ensure that only read-only tools are configured.
+ const existingExcludeTools = settings.merged.excludeTools || [];
+ const interactiveTools = [
+ ShellTool.Name,
+ EditTool.Name,
+ WriteFileTool.Name,
+ ];
+
+ const newExcludeTools = [
+ ...new Set([...existingExcludeTools, ...interactiveTools]),
+ ];
+
+ const nonInteractiveSettings = {
+ ...settings.merged,
+ excludeTools: newExcludeTools,
+ };
+ finalConfig = await loadCliConfig(
+ nonInteractiveSettings,
+ extensions,
+ config.getSessionId(),
+ argv,
+ );
+ await finalConfig.initialize();
+ }
+
+ return await validateNonInterActiveAuth(
+ settings.merged.selectedAuthType,
+ finalConfig,
+ );
+}
+
+async function validateNonInterActiveAuth(
+ selectedAuthType: AuthType | undefined,
+ nonInteractiveConfig: Config,
+) {
+ // making a special case for the cli. many headless environments might not have a settings.json set
+ // so if GEMINI_API_KEY is set, we'll use that. However since the oauth things are interactive anyway, we'll
+ // still expect that exists
+ if (!selectedAuthType && !process.env.GEMINI_API_KEY) {
+ console.error(
+ `Please set an Auth method in your ${USER_SETTINGS_PATH} OR specify GEMINI_API_KEY env variable file before running`,
+ );
+ process.exit(1);
+ }
+
+ selectedAuthType = selectedAuthType || AuthType.USE_GEMINI;
+ const err = validateAuthMethod(selectedAuthType);
+ if (err != null) {
+ console.error(err);
+ process.exit(1);
+ }
+
+ await nonInteractiveConfig.refreshAuth(selectedAuthType);
+ return nonInteractiveConfig;
+}
diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts
new file mode 100644
index 00000000..b22fea13
--- /dev/null
+++ b/packages/cli/src/nonInteractiveCli.test.ts
@@ -0,0 +1,344 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { runNonInteractive } from './nonInteractiveCli.js';
+import { Config, GeminiClient, ToolRegistry } from '@qwen/qwen-code-core';
+import { GenerateContentResponse, Part, FunctionCall } from '@google/genai';
+
+// Mock dependencies
+vi.mock('@qwen/qwen-code-core', async () => {
+ const actualCore = await vi.importActual<
+ typeof import('@qwen/qwen-code-core')
+ >('@qwen/qwen-code-core');
+ return {
+ ...actualCore,
+ GeminiClient: vi.fn(),
+ ToolRegistry: vi.fn(),
+ executeToolCall: vi.fn(),
+ };
+});
+
+describe('runNonInteractive', () => {
+ let mockConfig: Config;
+ let mockGeminiClient: GeminiClient;
+ let mockToolRegistry: ToolRegistry;
+ let mockChat: {
+ sendMessageStream: ReturnType;
+ };
+ let mockProcessStdoutWrite: ReturnType;
+ let mockProcessExit: ReturnType;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ mockChat = {
+ sendMessageStream: vi.fn(),
+ };
+ mockGeminiClient = {
+ getChat: vi.fn().mockResolvedValue(mockChat),
+ } as unknown as GeminiClient;
+ mockToolRegistry = {
+ getFunctionDeclarations: vi.fn().mockReturnValue([]),
+ getTool: vi.fn(),
+ } as unknown as ToolRegistry;
+
+ vi.mocked(GeminiClient).mockImplementation(() => mockGeminiClient);
+ vi.mocked(ToolRegistry).mockImplementation(() => mockToolRegistry);
+
+ mockConfig = {
+ getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
+ getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
+ getContentGeneratorConfig: vi.fn().mockReturnValue({}),
+ getMaxSessionTurns: vi.fn().mockReturnValue(10),
+ initialize: vi.fn(),
+ } as unknown as Config;
+
+ mockProcessStdoutWrite = vi.fn().mockImplementation(() => true);
+ process.stdout.write = mockProcessStdoutWrite as any; // Use any to bypass strict signature matching for mock
+ mockProcessExit = vi
+ .fn()
+ .mockImplementation((_code?: number) => undefined as never);
+ process.exit = mockProcessExit as any; // Use any for process.exit mock
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ // Restore original process methods if they were globally patched
+ // This might require storing the original methods before patching them in beforeEach
+ });
+
+ it('should process input and write text output', async () => {
+ const inputStream = (async function* () {
+ yield {
+ candidates: [{ content: { parts: [{ text: 'Hello' }] } }],
+ } as GenerateContentResponse;
+ yield {
+ candidates: [{ content: { parts: [{ text: ' World' }] } }],
+ } as GenerateContentResponse;
+ })();
+ mockChat.sendMessageStream.mockResolvedValue(inputStream);
+
+ await runNonInteractive(mockConfig, 'Test input', 'prompt-id-1');
+
+ expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
+ {
+ message: [{ text: 'Test input' }],
+ config: {
+ abortSignal: expect.any(AbortSignal),
+ tools: [{ functionDeclarations: [] }],
+ },
+ },
+ expect.any(String),
+ );
+ expect(mockProcessStdoutWrite).toHaveBeenCalledWith('Hello');
+ expect(mockProcessStdoutWrite).toHaveBeenCalledWith(' World');
+ expect(mockProcessStdoutWrite).toHaveBeenCalledWith('\n');
+ });
+
+ it('should handle a single tool call and respond', async () => {
+ const functionCall: FunctionCall = {
+ id: 'fc1',
+ name: 'testTool',
+ args: { p: 'v' },
+ };
+ const toolResponsePart: Part = {
+ functionResponse: {
+ name: 'testTool',
+ id: 'fc1',
+ response: { result: 'tool success' },
+ },
+ };
+
+ const { executeToolCall: mockCoreExecuteToolCall } = await import(
+ '@qwen/qwen-code-core'
+ );
+ vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
+ callId: 'fc1',
+ responseParts: [toolResponsePart],
+ resultDisplay: 'Tool success display',
+ error: undefined,
+ });
+
+ const stream1 = (async function* () {
+ yield { functionCalls: [functionCall] } as GenerateContentResponse;
+ })();
+ const stream2 = (async function* () {
+ yield {
+ candidates: [{ content: { parts: [{ text: 'Final answer' }] } }],
+ } as GenerateContentResponse;
+ })();
+ mockChat.sendMessageStream
+ .mockResolvedValueOnce(stream1)
+ .mockResolvedValueOnce(stream2);
+
+ await runNonInteractive(mockConfig, 'Use a tool', 'prompt-id-2');
+
+ expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(2);
+ expect(mockCoreExecuteToolCall).toHaveBeenCalledWith(
+ mockConfig,
+ expect.objectContaining({ callId: 'fc1', name: 'testTool' }),
+ mockToolRegistry,
+ expect.any(AbortSignal),
+ );
+ expect(mockChat.sendMessageStream).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ message: [toolResponsePart],
+ }),
+ expect.any(String),
+ );
+ expect(mockProcessStdoutWrite).toHaveBeenCalledWith('Final answer');
+ });
+
+ it('should handle error during tool execution', async () => {
+ const functionCall: FunctionCall = {
+ id: 'fcError',
+ name: 'errorTool',
+ args: {},
+ };
+ const errorResponsePart: Part = {
+ functionResponse: {
+ name: 'errorTool',
+ id: 'fcError',
+ response: { error: 'Tool failed' },
+ },
+ };
+
+ const { executeToolCall: mockCoreExecuteToolCall } = await import(
+ '@qwen/qwen-code-core'
+ );
+ vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
+ callId: 'fcError',
+ responseParts: [errorResponsePart],
+ resultDisplay: 'Tool execution failed badly',
+ error: new Error('Tool failed'),
+ });
+
+ const stream1 = (async function* () {
+ yield { functionCalls: [functionCall] } as GenerateContentResponse;
+ })();
+
+ const stream2 = (async function* () {
+ yield {
+ candidates: [
+ { content: { parts: [{ text: 'Could not complete request.' }] } },
+ ],
+ } as GenerateContentResponse;
+ })();
+ mockChat.sendMessageStream
+ .mockResolvedValueOnce(stream1)
+ .mockResolvedValueOnce(stream2);
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ await runNonInteractive(mockConfig, 'Trigger tool error', 'prompt-id-3');
+
+ expect(mockCoreExecuteToolCall).toHaveBeenCalled();
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Error executing tool errorTool: Tool execution failed badly',
+ );
+ expect(mockChat.sendMessageStream).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ message: [errorResponsePart],
+ }),
+ expect.any(String),
+ );
+ expect(mockProcessStdoutWrite).toHaveBeenCalledWith(
+ 'Could not complete request.',
+ );
+ });
+
+ it('should exit with error if sendMessageStream throws initially', async () => {
+ const apiError = new Error('API connection failed');
+ mockChat.sendMessageStream.mockRejectedValue(apiError);
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ await runNonInteractive(mockConfig, 'Initial fail', 'prompt-id-4');
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ '[API Error: API connection failed]',
+ );
+ });
+
+ it('should not exit if a tool is not found, and should send error back to model', async () => {
+ const functionCall: FunctionCall = {
+ id: 'fcNotFound',
+ name: 'nonExistentTool',
+ args: {},
+ };
+ const errorResponsePart: Part = {
+ functionResponse: {
+ name: 'nonExistentTool',
+ id: 'fcNotFound',
+ response: { error: 'Tool "nonExistentTool" not found in registry.' },
+ },
+ };
+
+ const { executeToolCall: mockCoreExecuteToolCall } = await import(
+ '@qwen/qwen-code-core'
+ );
+ vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
+ callId: 'fcNotFound',
+ responseParts: [errorResponsePart],
+ resultDisplay: 'Tool "nonExistentTool" not found in registry.',
+ error: new Error('Tool "nonExistentTool" not found in registry.'),
+ });
+
+ const stream1 = (async function* () {
+ yield { functionCalls: [functionCall] } as GenerateContentResponse;
+ })();
+ const stream2 = (async function* () {
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: 'Unfortunately the tool does not exist.' }],
+ },
+ },
+ ],
+ } as GenerateContentResponse;
+ })();
+ mockChat.sendMessageStream
+ .mockResolvedValueOnce(stream1)
+ .mockResolvedValueOnce(stream2);
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ await runNonInteractive(
+ mockConfig,
+ 'Trigger tool not found',
+ 'prompt-id-5',
+ );
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Error executing tool nonExistentTool: Tool "nonExistentTool" not found in registry.',
+ );
+
+ expect(mockProcessExit).not.toHaveBeenCalled();
+
+ expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(2);
+ expect(mockChat.sendMessageStream).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ message: [errorResponsePart],
+ }),
+ expect.any(String),
+ );
+
+ expect(mockProcessStdoutWrite).toHaveBeenCalledWith(
+ 'Unfortunately the tool does not exist.',
+ );
+ });
+
+ it('should exit when max session turns are exceeded', async () => {
+ const functionCall: FunctionCall = {
+ id: 'fcLoop',
+ name: 'loopTool',
+ args: {},
+ };
+ const toolResponsePart: Part = {
+ functionResponse: {
+ name: 'loopTool',
+ id: 'fcLoop',
+ response: { result: 'still looping' },
+ },
+ };
+
+ // Config with a max turn of 1
+ vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(1);
+
+ const { executeToolCall: mockCoreExecuteToolCall } = await import(
+ '@qwen/qwen-code-core'
+ );
+ vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
+ callId: 'fcLoop',
+ responseParts: [toolResponsePart],
+ resultDisplay: 'Still looping',
+ error: undefined,
+ });
+
+ const stream = (async function* () {
+ yield { functionCalls: [functionCall] } as GenerateContentResponse;
+ })();
+
+ mockChat.sendMessageStream.mockResolvedValue(stream);
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ await runNonInteractive(mockConfig, 'Trigger loop');
+
+ expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(1);
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ `
+ Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.`,
+ );
+ expect(mockProcessExit).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts
new file mode 100644
index 00000000..c0628746
--- /dev/null
+++ b/packages/cli/src/nonInteractiveCli.ts
@@ -0,0 +1,275 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ Config,
+ ToolCallRequestInfo,
+ executeToolCall,
+ ToolRegistry,
+ shutdownTelemetry,
+ isTelemetrySdkInitialized,
+ ToolResultDisplay,
+} from '@qwen/qwen-code-core';
+import {
+ Content,
+ Part,
+ FunctionCall,
+ GenerateContentResponse,
+} from '@google/genai';
+
+import { parseAndFormatApiError } from './ui/utils/errorParsing.js';
+
+function getResponseText(response: GenerateContentResponse): string | null {
+ if (response.candidates && response.candidates.length > 0) {
+ const candidate = response.candidates[0];
+ if (
+ candidate.content &&
+ candidate.content.parts &&
+ candidate.content.parts.length > 0
+ ) {
+ // We are running in headless mode so we don't need to return thoughts to STDOUT.
+ const thoughtPart = candidate.content.parts[0];
+ if (thoughtPart?.thought) {
+ return null;
+ }
+ return candidate.content.parts
+ .filter((part) => part.text)
+ .map((part) => part.text)
+ .join('');
+ }
+ }
+ return null;
+}
+
+// Helper function to format tool call arguments for display
+function formatToolArgs(args: Record): string {
+ if (!args || Object.keys(args).length === 0) {
+ return '(no arguments)';
+ }
+
+ const formattedArgs = Object.entries(args)
+ .map(([key, value]) => {
+ if (typeof value === 'string') {
+ return `${key}: "${value}"`;
+ } else if (typeof value === 'object' && value !== null) {
+ return `${key}: ${JSON.stringify(value)}`;
+ } else {
+ return `${key}: ${value}`;
+ }
+ })
+ .join(', ');
+
+ return `(${formattedArgs})`;
+}
+// Helper function to display tool call information
+function displayToolCallInfo(
+ toolName: string,
+ args: Record,
+ status: 'start' | 'success' | 'error',
+ resultDisplay?: ToolResultDisplay,
+ errorMessage?: string,
+): void {
+ const timestamp = new Date().toLocaleTimeString();
+ const argsStr = formatToolArgs(args);
+
+ switch (status) {
+ case 'start':
+ process.stdout.write(
+ `\n[${timestamp}] ๐ง Executing tool: ${toolName} ${argsStr}\n`,
+ );
+ break;
+ case 'success':
+ if (resultDisplay) {
+ if (typeof resultDisplay === 'string' && resultDisplay.trim()) {
+ process.stdout.write(
+ `[${timestamp}] โ
Tool ${toolName} completed successfully\n`,
+ );
+ process.stdout.write(`๐ Result:\n${resultDisplay}\n`);
+ } else if (
+ typeof resultDisplay === 'object' &&
+ 'fileDiff' in resultDisplay
+ ) {
+ process.stdout.write(
+ `[${timestamp}] โ
Tool ${toolName} completed successfully\n`,
+ );
+ process.stdout.write(`๐ File: ${resultDisplay.fileName}\n`);
+ process.stdout.write(`๐ Diff:\n${resultDisplay.fileDiff}\n`);
+ } else {
+ process.stdout.write(
+ `[${timestamp}] โ
Tool ${toolName} completed successfully (no output)\n`,
+ );
+ }
+ } else {
+ process.stdout.write(
+ `[${timestamp}] โ
Tool ${toolName} completed successfully (no output)\n`,
+ );
+ }
+ break;
+ case 'error':
+ process.stdout.write(
+ `[${timestamp}] โ Tool ${toolName} failed: ${errorMessage}\n`,
+ );
+ break;
+ default:
+ process.stdout.write(
+ `[${timestamp}] โ ๏ธ Tool ${toolName} reported unknown status: ${status}\n`,
+ );
+ break;
+ }
+}
+
+export async function runNonInteractive(
+ config: Config,
+ input: string,
+ prompt_id: string,
+): Promise {
+ await config.initialize();
+ // Handle EPIPE errors when the output is piped to a command that closes early.
+ process.stdout.on('error', (err: NodeJS.ErrnoException) => {
+ if (err.code === 'EPIPE') {
+ // Exit gracefully if the pipe is closed.
+ process.exit(0);
+ }
+ });
+
+ const geminiClient = config.getGeminiClient();
+ const toolRegistry: ToolRegistry = await config.getToolRegistry();
+
+ const chat = await geminiClient.getChat();
+ const abortController = new AbortController();
+ let currentMessages: Content[] = [{ role: 'user', parts: [{ text: input }] }];
+ let turnCount = 0;
+ try {
+ while (true) {
+ turnCount++;
+ if (
+ config.getMaxSessionTurns() > 0 &&
+ turnCount > config.getMaxSessionTurns()
+ ) {
+ console.error(
+ '\n Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
+ );
+ return;
+ }
+ const functionCalls: FunctionCall[] = [];
+
+ const responseStream = await chat.sendMessageStream(
+ {
+ message: currentMessages[0]?.parts || [], // Ensure parts are always provided
+ config: {
+ abortSignal: abortController.signal,
+ tools: [
+ { functionDeclarations: toolRegistry.getFunctionDeclarations() },
+ ],
+ },
+ },
+ prompt_id,
+ );
+
+ for await (const resp of responseStream) {
+ if (abortController.signal.aborted) {
+ console.error('Operation cancelled.');
+ return;
+ }
+ const textPart = getResponseText(resp);
+ if (textPart) {
+ process.stdout.write(textPart);
+ }
+ if (resp.functionCalls) {
+ functionCalls.push(...resp.functionCalls);
+ }
+ }
+
+ if (functionCalls.length > 0) {
+ const toolResponseParts: Part[] = [];
+
+ for (const fc of functionCalls) {
+ const callId = fc.id ?? `${fc.name}-${Date.now()}`;
+ const requestInfo: ToolCallRequestInfo = {
+ callId,
+ name: fc.name as string,
+ args: (fc.args ?? {}) as Record,
+ isClientInitiated: false,
+ prompt_id,
+ };
+
+ //Display tool call start information
+ displayToolCallInfo(fc.name as string, fc.args ?? {}, 'start');
+
+ const toolResponse = await executeToolCall(
+ config,
+ requestInfo,
+ toolRegistry,
+ abortController.signal,
+ );
+
+ if (toolResponse.error) {
+ // Display tool call error information
+ const errorMessage =
+ typeof toolResponse.resultDisplay === 'string'
+ ? toolResponse.resultDisplay
+ : toolResponse.error?.message;
+
+ displayToolCallInfo(
+ fc.name as string,
+ fc.args ?? {},
+ 'error',
+ undefined,
+ errorMessage,
+ );
+
+ const isToolNotFound = toolResponse.error.message.includes(
+ 'not found in registry',
+ );
+ console.error(
+ `Error executing tool ${fc.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`,
+ );
+ if (!isToolNotFound) {
+ process.exit(1);
+ }
+ } else {
+ // Display tool call success information
+ displayToolCallInfo(
+ fc.name as string,
+ fc.args ?? {},
+ 'success',
+ toolResponse.resultDisplay,
+ );
+ }
+
+ if (toolResponse.responseParts) {
+ const parts = Array.isArray(toolResponse.responseParts)
+ ? toolResponse.responseParts
+ : [toolResponse.responseParts];
+ for (const part of parts) {
+ if (typeof part === 'string') {
+ toolResponseParts.push({ text: part });
+ } else if (part) {
+ toolResponseParts.push(part);
+ }
+ }
+ }
+ }
+ currentMessages = [{ role: 'user', parts: toolResponseParts }];
+ } else {
+ process.stdout.write('\n'); // Ensure a final newline
+ return;
+ }
+ }
+ } catch (error) {
+ console.error(
+ parseAndFormatApiError(
+ error,
+ config.getContentGeneratorConfig()?.authType,
+ ),
+ );
+ process.exit(1);
+ } finally {
+ if (isTelemetrySdkInitialized()) {
+ await shutdownTelemetry();
+ }
+ }
+}
diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts
new file mode 100644
index 00000000..e780ec5f
--- /dev/null
+++ b/packages/cli/src/services/CommandService.test.ts
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach } from 'vitest';
+import { CommandService } from './CommandService.js';
+import { type SlashCommand } from '../ui/commands/types.js';
+import { memoryCommand } from '../ui/commands/memoryCommand.js';
+import { helpCommand } from '../ui/commands/helpCommand.js';
+import { clearCommand } from '../ui/commands/clearCommand.js';
+import { authCommand } from '../ui/commands/authCommand.js';
+import { themeCommand } from '../ui/commands/themeCommand.js';
+import { privacyCommand } from '../ui/commands/privacyCommand.js';
+import { aboutCommand } from '../ui/commands/aboutCommand.js';
+
+// Mock the command modules to isolate the service from the command implementations.
+vi.mock('../ui/commands/memoryCommand.js', () => ({
+ memoryCommand: { name: 'memory', description: 'Mock Memory' },
+}));
+vi.mock('../ui/commands/helpCommand.js', () => ({
+ helpCommand: { name: 'help', description: 'Mock Help' },
+}));
+vi.mock('../ui/commands/clearCommand.js', () => ({
+ clearCommand: { name: 'clear', description: 'Mock Clear' },
+}));
+vi.mock('../ui/commands/authCommand.js', () => ({
+ authCommand: { name: 'auth', description: 'Mock Auth' },
+}));
+vi.mock('../ui/commands/themeCommand.js', () => ({
+ themeCommand: { name: 'theme', description: 'Mock Theme' },
+}));
+vi.mock('../ui/commands/privacyCommand.js', () => ({
+ privacyCommand: { name: 'privacy', description: 'Mock Privacy' },
+}));
+vi.mock('../ui/commands/aboutCommand.js', () => ({
+ aboutCommand: { name: 'about', description: 'Mock About' },
+}));
+
+describe('CommandService', () => {
+ describe('when using default production loader', () => {
+ let commandService: CommandService;
+
+ beforeEach(() => {
+ commandService = new CommandService();
+ });
+
+ it('should initialize with an empty command tree', () => {
+ const tree = commandService.getCommands();
+ expect(tree).toBeInstanceOf(Array);
+ expect(tree.length).toBe(0);
+ });
+
+ describe('loadCommands', () => {
+ it('should load the built-in commands into the command tree', async () => {
+ // Pre-condition check
+ expect(commandService.getCommands().length).toBe(0);
+
+ // Action
+ await commandService.loadCommands();
+ const tree = commandService.getCommands();
+
+ // Post-condition assertions
+ expect(tree.length).toBe(7);
+
+ const commandNames = tree.map((cmd) => cmd.name);
+ expect(commandNames).toContain('auth');
+ expect(commandNames).toContain('memory');
+ expect(commandNames).toContain('help');
+ expect(commandNames).toContain('clear');
+ expect(commandNames).toContain('theme');
+ expect(commandNames).toContain('privacy');
+ expect(commandNames).toContain('about');
+ });
+
+ it('should overwrite any existing commands when called again', async () => {
+ // Load once
+ await commandService.loadCommands();
+ expect(commandService.getCommands().length).toBe(7);
+
+ // Load again
+ await commandService.loadCommands();
+ const tree = commandService.getCommands();
+
+ // Should not append, but overwrite
+ expect(tree.length).toBe(7);
+ });
+ });
+
+ describe('getCommandTree', () => {
+ it('should return the current command tree', async () => {
+ const initialTree = commandService.getCommands();
+ expect(initialTree).toEqual([]);
+
+ await commandService.loadCommands();
+
+ const loadedTree = commandService.getCommands();
+ expect(loadedTree.length).toBe(7);
+ expect(loadedTree).toEqual([
+ aboutCommand,
+ authCommand,
+ clearCommand,
+ helpCommand,
+ memoryCommand,
+ privacyCommand,
+ themeCommand,
+ ]);
+ });
+ });
+ });
+
+ describe('when initialized with an injected loader function', () => {
+ it('should use the provided loader instead of the built-in one', async () => {
+ // Arrange: Create a set of mock commands.
+ const mockCommands: SlashCommand[] = [
+ { name: 'injected-test-1', description: 'injected 1' },
+ { name: 'injected-test-2', description: 'injected 2' },
+ ];
+
+ // Arrange: Create a mock loader FUNCTION that resolves with our mock commands.
+ const mockLoader = vi.fn().mockResolvedValue(mockCommands);
+
+ // Act: Instantiate the service WITH the injected loader function.
+ const commandService = new CommandService(mockLoader);
+ await commandService.loadCommands();
+ const tree = commandService.getCommands();
+
+ // Assert: The tree should contain ONLY our injected commands.
+ expect(mockLoader).toHaveBeenCalled(); // Verify our mock loader was actually called.
+ expect(tree.length).toBe(2);
+ expect(tree).toEqual(mockCommands);
+
+ const commandNames = tree.map((cmd) => cmd.name);
+ expect(commandNames).not.toContain('memory'); // Verify it didn't load production commands.
+ });
+ });
+});
diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts
new file mode 100644
index 00000000..ef31952d
--- /dev/null
+++ b/packages/cli/src/services/CommandService.ts
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SlashCommand } from '../ui/commands/types.js';
+import { memoryCommand } from '../ui/commands/memoryCommand.js';
+import { helpCommand } from '../ui/commands/helpCommand.js';
+import { clearCommand } from '../ui/commands/clearCommand.js';
+import { authCommand } from '../ui/commands/authCommand.js';
+import { themeCommand } from '../ui/commands/themeCommand.js';
+import { privacyCommand } from '../ui/commands/privacyCommand.js';
+import { aboutCommand } from '../ui/commands/aboutCommand.js';
+
+const loadBuiltInCommands = async (): Promise => [
+ aboutCommand,
+ authCommand,
+ clearCommand,
+ helpCommand,
+ memoryCommand,
+ privacyCommand,
+ themeCommand,
+];
+
+export class CommandService {
+ private commands: SlashCommand[] = [];
+
+ constructor(
+ private commandLoader: () => Promise = loadBuiltInCommands,
+ ) {
+ // The constructor can be used for dependency injection in the future.
+ }
+
+ async loadCommands(): Promise {
+ // For now, we only load the built-in commands.
+ // File-based and remote commands will be added later.
+ this.commands = await this.commandLoader();
+ }
+
+ getCommands(): SlashCommand[] {
+ return this.commands;
+ }
+}
diff --git a/packages/cli/src/test-utils/mockCommandContext.test.ts b/packages/cli/src/test-utils/mockCommandContext.test.ts
new file mode 100644
index 00000000..310bf748
--- /dev/null
+++ b/packages/cli/src/test-utils/mockCommandContext.test.ts
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect } from 'vitest';
+import { createMockCommandContext } from './mockCommandContext.js';
+
+describe('createMockCommandContext', () => {
+ it('should return a valid CommandContext object with default mocks', () => {
+ const context = createMockCommandContext();
+
+ // Just a few spot checks to ensure the structure is correct
+ // and functions are mocks.
+ expect(context).toBeDefined();
+ expect(context.ui.addItem).toBeInstanceOf(Function);
+ expect(vi.isMockFunction(context.ui.addItem)).toBe(true);
+ });
+
+ it('should apply top-level overrides correctly', () => {
+ const mockClear = vi.fn();
+ const overrides = {
+ ui: {
+ clear: mockClear,
+ },
+ };
+
+ const context = createMockCommandContext(overrides);
+
+ // Call the function to see if the override was used
+ context.ui.clear();
+
+ // Assert that our specific mock was called, not the default
+ expect(mockClear).toHaveBeenCalled();
+ // And that other defaults are still in place
+ expect(vi.isMockFunction(context.ui.addItem)).toBe(true);
+ });
+
+ it('should apply deeply nested overrides correctly', () => {
+ // This is the most important test for factory's logic.
+ const mockConfig = {
+ getProjectRoot: () => '/test/project',
+ getModel: () => 'gemini-pro',
+ };
+
+ const overrides = {
+ services: {
+ config: mockConfig,
+ },
+ };
+
+ const context = createMockCommandContext(overrides);
+
+ expect(context.services.config).toBeDefined();
+ expect(context.services.config?.getModel()).toBe('gemini-pro');
+ expect(context.services.config?.getProjectRoot()).toBe('/test/project');
+
+ // Verify a default property on the same nested object is still there
+ expect(context.services.logger).toBeDefined();
+ });
+});
diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts
new file mode 100644
index 00000000..bf7d814d
--- /dev/null
+++ b/packages/cli/src/test-utils/mockCommandContext.ts
@@ -0,0 +1,94 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi } from 'vitest';
+import { CommandContext } from '../ui/commands/types.js';
+import { LoadedSettings } from '../config/settings.js';
+import { GitService } from '@google/gemini-cli-core';
+import { SessionStatsState } from '../ui/contexts/SessionContext.js';
+
+// A utility type to make all properties of an object, and its nested objects, partial.
+type DeepPartial = T extends object
+ ? {
+ [P in keyof T]?: DeepPartial;
+ }
+ : T;
+
+/**
+ * Creates a deep, fully-typed mock of the CommandContext for use in tests.
+ * All functions are pre-mocked with `vi.fn()`.
+ *
+ * @param overrides - A deep partial object to override any default mock values.
+ * @returns A complete, mocked CommandContext object.
+ */
+export const createMockCommandContext = (
+ overrides: DeepPartial = {},
+): CommandContext => {
+ const defaultMocks: CommandContext = {
+ services: {
+ config: null,
+ settings: { merged: {} } as LoadedSettings,
+ git: undefined as GitService | undefined,
+ logger: {
+ log: vi.fn(),
+ logMessage: vi.fn(),
+ saveCheckpoint: vi.fn(),
+ loadCheckpoint: vi.fn().mockResolvedValue([]),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any, // Cast because Logger is a class.
+ },
+ ui: {
+ addItem: vi.fn(),
+ clear: vi.fn(),
+ setDebugMessage: vi.fn(),
+ },
+ session: {
+ stats: {
+ sessionStartTime: new Date(),
+ lastPromptTokenCount: 0,
+ metrics: {
+ models: {},
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ },
+ } as SessionStatsState,
+ },
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const merge = (target: any, source: any): any => {
+ const output = { ...target };
+
+ for (const key in source) {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ const sourceValue = source[key];
+ const targetValue = output[key];
+
+ if (
+ sourceValue &&
+ typeof sourceValue === 'object' &&
+ !Array.isArray(sourceValue) &&
+ targetValue &&
+ typeof targetValue === 'object' &&
+ !Array.isArray(targetValue)
+ ) {
+ output[key] = merge(targetValue, sourceValue);
+ } else {
+ output[key] = sourceValue;
+ }
+ }
+ }
+ return output;
+ };
+
+ return merge(defaultMocks, overrides);
+};
diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx
new file mode 100644
index 00000000..6336f7fc
--- /dev/null
+++ b/packages/cli/src/ui/App.test.tsx
@@ -0,0 +1,595 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
+import { render } from 'ink-testing-library';
+import { AppWrapper as App } from './App.js';
+import {
+ Config as ServerConfig,
+ MCPServerConfig,
+ ApprovalMode,
+ ToolRegistry,
+ AccessibilitySettings,
+ SandboxConfig,
+ GeminiClient,
+} from '@qwen/qwen-code-core';
+import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
+import process from 'node:process';
+import { useGeminiStream } from './hooks/useGeminiStream.js';
+import { StreamingState } from './types.js';
+import { Tips } from './components/Tips.js';
+
+// Define a more complete mock server config based on actual Config
+interface MockServerConfig {
+ apiKey: string;
+ model: string;
+ sandbox?: SandboxConfig;
+ targetDir: string;
+ debugMode: boolean;
+ question?: string;
+ fullContext: boolean;
+ coreTools?: string[];
+ toolDiscoveryCommand?: string;
+ toolCallCommand?: string;
+ mcpServerCommand?: string;
+ mcpServers?: Record; // Use imported MCPServerConfig
+ userAgent: string;
+ userMemory: string;
+ geminiMdFileCount: number;
+ approvalMode: ApprovalMode;
+ vertexai?: boolean;
+ showMemoryUsage?: boolean;
+ accessibility?: AccessibilitySettings;
+ embeddingModel: string;
+
+ getApiKey: Mock<() => string>;
+ getModel: Mock<() => string>;
+ getSandbox: Mock<() => SandboxConfig | undefined>;
+ getTargetDir: Mock<() => string>;
+ getToolRegistry: Mock<() => ToolRegistry>; // Use imported ToolRegistry type
+ getDebugMode: Mock<() => boolean>;
+ getQuestion: Mock<() => string | undefined>;
+ getFullContext: Mock<() => boolean>;
+ getCoreTools: Mock<() => string[] | undefined>;
+ getToolDiscoveryCommand: Mock<() => string | undefined>;
+ getToolCallCommand: Mock<() => string | undefined>;
+ getMcpServerCommand: Mock<() => string | undefined>;
+ getMcpServers: Mock<() => Record | undefined>;
+ getUserAgent: Mock<() => string>;
+ getUserMemory: Mock<() => string>;
+ setUserMemory: Mock<(newUserMemory: string) => void>;
+ getGeminiMdFileCount: Mock<() => number>;
+ setGeminiMdFileCount: Mock<(count: number) => void>;
+ getApprovalMode: Mock<() => ApprovalMode>;
+ setApprovalMode: Mock<(skip: ApprovalMode) => void>;
+ getVertexAI: Mock<() => boolean | undefined>;
+ getShowMemoryUsage: Mock<() => boolean>;
+ getAccessibility: Mock<() => AccessibilitySettings>;
+ getProjectRoot: Mock<() => string | undefined>;
+ getAllGeminiMdFilenames: Mock<() => string[]>;
+ getGeminiClient: Mock<() => GeminiClient | undefined>;
+ getUserTier: Mock<() => Promise>;
+}
+
+// Mock @qwen/qwen-code-core and its Config class
+vi.mock('@qwen/qwen-code-core', async (importOriginal) => {
+ const actualCore =
+ await importOriginal();
+ const ConfigClassMock = vi
+ .fn()
+ .mockImplementation((optionsPassedToConstructor) => {
+ const opts = { ...optionsPassedToConstructor }; // Clone
+ // Basic mock structure, will be extended by the instance in tests
+ return {
+ apiKey: opts.apiKey || 'test-key',
+ model: opts.model || 'test-model-in-mock-factory',
+ sandbox: opts.sandbox,
+ targetDir: opts.targetDir || '/test/dir',
+ debugMode: opts.debugMode || false,
+ question: opts.question,
+ fullContext: opts.fullContext ?? false,
+ coreTools: opts.coreTools,
+ toolDiscoveryCommand: opts.toolDiscoveryCommand,
+ toolCallCommand: opts.toolCallCommand,
+ mcpServerCommand: opts.mcpServerCommand,
+ mcpServers: opts.mcpServers,
+ userAgent: opts.userAgent || 'test-agent',
+ userMemory: opts.userMemory || '',
+ geminiMdFileCount: opts.geminiMdFileCount || 0,
+ approvalMode: opts.approvalMode ?? ApprovalMode.DEFAULT,
+ vertexai: opts.vertexai,
+ showMemoryUsage: opts.showMemoryUsage ?? false,
+ accessibility: opts.accessibility ?? {},
+ embeddingModel: opts.embeddingModel || 'test-embedding-model',
+
+ getApiKey: vi.fn(() => opts.apiKey || 'test-key'),
+ getModel: vi.fn(() => opts.model || 'test-model-in-mock-factory'),
+ getSandbox: vi.fn(() => opts.sandbox),
+ getTargetDir: vi.fn(() => opts.targetDir || '/test/dir'),
+ getToolRegistry: vi.fn(() => ({}) as ToolRegistry), // Simple mock
+ getDebugMode: vi.fn(() => opts.debugMode || false),
+ getQuestion: vi.fn(() => opts.question),
+ getFullContext: vi.fn(() => opts.fullContext ?? false),
+ getCoreTools: vi.fn(() => opts.coreTools),
+ getToolDiscoveryCommand: vi.fn(() => opts.toolDiscoveryCommand),
+ getToolCallCommand: vi.fn(() => opts.toolCallCommand),
+ getMcpServerCommand: vi.fn(() => opts.mcpServerCommand),
+ getMcpServers: vi.fn(() => opts.mcpServers),
+ getUserAgent: vi.fn(() => opts.userAgent || 'test-agent'),
+ getUserMemory: vi.fn(() => opts.userMemory || ''),
+ setUserMemory: vi.fn(),
+ getGeminiMdFileCount: vi.fn(() => opts.geminiMdFileCount || 0),
+ setGeminiMdFileCount: vi.fn(),
+ getApprovalMode: vi.fn(() => opts.approvalMode ?? ApprovalMode.DEFAULT),
+ setApprovalMode: vi.fn(),
+ getVertexAI: vi.fn(() => opts.vertexai),
+ getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false),
+ getAccessibility: vi.fn(() => opts.accessibility ?? {}),
+ getProjectRoot: vi.fn(() => opts.targetDir),
+ getGeminiClient: vi.fn(() => ({})),
+ getCheckpointingEnabled: vi.fn(() => opts.checkpointing ?? true),
+ getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
+ setFlashFallbackHandler: vi.fn(),
+ getSessionId: vi.fn(() => 'test-session-id'),
+ getUserTier: vi.fn().mockResolvedValue(undefined),
+ };
+ });
+ return {
+ ...actualCore,
+ Config: ConfigClassMock,
+ MCPServerConfig: actualCore.MCPServerConfig,
+ getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
+ };
+});
+
+// Mock heavy dependencies or those with side effects
+vi.mock('./hooks/useGeminiStream', () => ({
+ useGeminiStream: vi.fn(() => ({
+ streamingState: 'Idle',
+ submitQuery: vi.fn(),
+ initError: null,
+ pendingHistoryItems: [],
+ })),
+}));
+
+vi.mock('./hooks/useAuthCommand', () => ({
+ useAuthCommand: vi.fn(() => ({
+ isAuthDialogOpen: false,
+ openAuthDialog: vi.fn(),
+ handleAuthSelect: vi.fn(),
+ handleAuthHighlight: vi.fn(),
+ isAuthenticating: false,
+ cancelAuthentication: vi.fn(),
+ })),
+}));
+
+vi.mock('./hooks/useLogger', () => ({
+ useLogger: vi.fn(() => ({
+ getPreviousUserMessages: vi.fn().mockResolvedValue([]),
+ })),
+}));
+
+vi.mock('../config/config.js', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ // @ts-expect-error - this is fine
+ ...actual,
+ loadHierarchicalGeminiMemory: vi
+ .fn()
+ .mockResolvedValue({ memoryContent: '', fileCount: 0 }),
+ };
+});
+
+vi.mock('./components/Tips.js', () => ({
+ Tips: vi.fn(() => null),
+}));
+
+vi.mock('./components/Header.js', () => ({
+ Header: vi.fn(() => null),
+}));
+
+describe('App UI', () => {
+ let mockConfig: MockServerConfig;
+ let mockSettings: LoadedSettings;
+ let mockVersion: string;
+ let currentUnmount: (() => void) | undefined;
+
+ const createMockSettings = (
+ settings: {
+ system?: Partial;
+ user?: Partial;
+ workspace?: Partial;
+ } = {},
+ ): LoadedSettings => {
+ const systemSettingsFile: SettingsFile = {
+ path: '/system/settings.json',
+ settings: settings.system || {},
+ };
+ const userSettingsFile: SettingsFile = {
+ path: '/user/settings.json',
+ settings: settings.user || {},
+ };
+ const workspaceSettingsFile: SettingsFile = {
+ path: '/workspace/.qwen/settings.json',
+ settings: settings.workspace || {},
+ };
+ return new LoadedSettings(
+ systemSettingsFile,
+ userSettingsFile,
+ workspaceSettingsFile,
+ [],
+ );
+ };
+
+ beforeEach(() => {
+ const ServerConfigMocked = vi.mocked(ServerConfig, true);
+ mockConfig = new ServerConfigMocked({
+ embeddingModel: 'test-embedding-model',
+ sandbox: undefined,
+ targetDir: '/test/dir',
+ debugMode: false,
+ userMemory: '',
+ geminiMdFileCount: 0,
+ showMemoryUsage: false,
+ sessionId: 'test-session-id',
+ cwd: '/tmp',
+ model: 'model',
+ }) as unknown as MockServerConfig;
+ mockVersion = '0.0.0-test';
+
+ // Ensure the getShowMemoryUsage mock function is specifically set up if not covered by constructor mock
+ if (!mockConfig.getShowMemoryUsage) {
+ mockConfig.getShowMemoryUsage = vi.fn(() => false);
+ }
+ mockConfig.getShowMemoryUsage.mockReturnValue(false); // Default for most tests
+
+ // Ensure a theme is set so the theme dialog does not appear.
+ mockSettings = createMockSettings({ workspace: { theme: 'Default' } });
+ });
+
+ afterEach(() => {
+ if (currentUnmount) {
+ currentUnmount();
+ currentUnmount = undefined;
+ }
+ vi.clearAllMocks(); // Clear mocks after each test
+ });
+
+ it('should display default "GEMINI.md" in footer when contextFileName is not set and count is 1', async () => {
+ mockConfig.getGeminiMdFileCount.mockReturnValue(1);
+ // For this test, ensure showMemoryUsage is false or debugMode is false if it relies on that
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve(); // Wait for any async updates
+ expect(lastFrame()).toContain('Using 1 GEMINI.md file');
+ });
+
+ it('should display default "GEMINI.md" with plural when contextFileName is not set and count is > 1', async () => {
+ mockConfig.getGeminiMdFileCount.mockReturnValue(2);
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(lastFrame()).toContain('Using 2 GEMINI.md files');
+ });
+
+ it('should display custom contextFileName in footer when set and count is 1', async () => {
+ mockSettings = createMockSettings({
+ workspace: { contextFileName: 'AGENTS.md', theme: 'Default' },
+ });
+ mockConfig.getGeminiMdFileCount.mockReturnValue(1);
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(lastFrame()).toContain('Using 1 AGENTS.md file');
+ });
+
+ it('should display a generic message when multiple context files with different names are provided', async () => {
+ mockSettings = createMockSettings({
+ workspace: {
+ contextFileName: ['AGENTS.md', 'CONTEXT.md'],
+ theme: 'Default',
+ },
+ });
+ mockConfig.getGeminiMdFileCount.mockReturnValue(2);
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(lastFrame()).toContain('Using 2 context files');
+ });
+
+ it('should display custom contextFileName with plural when set and count is > 1', async () => {
+ mockSettings = createMockSettings({
+ workspace: { contextFileName: 'MY_NOTES.TXT', theme: 'Default' },
+ });
+ mockConfig.getGeminiMdFileCount.mockReturnValue(3);
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(lastFrame()).toContain('Using 3 MY_NOTES.TXT files');
+ });
+
+ it('should not display context file message if count is 0, even if contextFileName is set', async () => {
+ mockSettings = createMockSettings({
+ workspace: { contextFileName: 'ANY_FILE.MD', theme: 'Default' },
+ });
+ mockConfig.getGeminiMdFileCount.mockReturnValue(0);
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(lastFrame()).not.toContain('ANY_FILE.MD');
+ });
+
+ it('should display GEMINI.md and MCP server count when both are present', async () => {
+ mockConfig.getGeminiMdFileCount.mockReturnValue(2);
+ mockConfig.getMcpServers.mockReturnValue({
+ server1: {} as MCPServerConfig,
+ });
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(lastFrame()).toContain('server');
+ });
+
+ it('should display only MCP server count when GEMINI.md count is 0', async () => {
+ mockConfig.getGeminiMdFileCount.mockReturnValue(0);
+ mockConfig.getMcpServers.mockReturnValue({
+ server1: {} as MCPServerConfig,
+ server2: {} as MCPServerConfig,
+ });
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(lastFrame()).toContain('Using 2 MCP servers');
+ });
+
+ it('should display Tips component by default', async () => {
+ const { unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(vi.mocked(Tips)).toHaveBeenCalled();
+ });
+
+ it('should not display Tips component when hideTips is true', async () => {
+ mockSettings = createMockSettings({
+ workspace: {
+ hideTips: true,
+ },
+ });
+
+ const { unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(vi.mocked(Tips)).not.toHaveBeenCalled();
+ });
+
+ it('should display Header component by default', async () => {
+ const { Header } = await import('./components/Header.js');
+ const { unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(vi.mocked(Header)).toHaveBeenCalled();
+ });
+
+ it('should not display Header component when hideBanner is true', async () => {
+ const { Header } = await import('./components/Header.js');
+ mockSettings = createMockSettings({
+ user: { hideBanner: true },
+ });
+
+ const { unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(vi.mocked(Header)).not.toHaveBeenCalled();
+ });
+
+ it('should show tips if system says show, but workspace and user settings say hide', async () => {
+ mockSettings = createMockSettings({
+ system: { hideTips: false },
+ user: { hideTips: true },
+ workspace: { hideTips: true },
+ });
+
+ const { unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(vi.mocked(Tips)).toHaveBeenCalled();
+ });
+
+ describe('when no theme is set', () => {
+ let originalNoColor: string | undefined;
+
+ beforeEach(() => {
+ originalNoColor = process.env.NO_COLOR;
+ // Ensure no theme is set for these tests
+ mockSettings = createMockSettings({});
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+ });
+
+ afterEach(() => {
+ process.env.NO_COLOR = originalNoColor;
+ });
+
+ it('should display theme dialog if NO_COLOR is not set', async () => {
+ delete process.env.NO_COLOR;
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+
+ expect(lastFrame()).toContain('Select Theme');
+ });
+
+ it('should display a message if NO_COLOR is set', async () => {
+ process.env.NO_COLOR = 'true';
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+
+ expect(lastFrame()).toContain(
+ 'Theme configuration unavailable due to NO_COLOR env variable.',
+ );
+ expect(lastFrame()).not.toContain('Select Theme');
+ });
+ });
+
+ describe('with initial prompt from --prompt-interactive', () => {
+ it('should submit the initial prompt automatically', async () => {
+ const mockSubmitQuery = vi.fn();
+
+ mockConfig.getQuestion = vi.fn(() => 'hello from prompt-interactive');
+
+ vi.mocked(useGeminiStream).mockReturnValue({
+ streamingState: StreamingState.Idle,
+ submitQuery: mockSubmitQuery,
+ initError: null,
+ pendingHistoryItems: [],
+ thought: null,
+ });
+
+ mockConfig.getGeminiClient.mockReturnValue({
+ isInitialized: vi.fn(() => true),
+ } as unknown as GeminiClient);
+
+ const { unmount, rerender } = render(
+ ,
+ );
+ currentUnmount = unmount;
+
+ // Force a re-render to trigger useEffect
+ rerender(
+ ,
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ expect(mockSubmitQuery).toHaveBeenCalledWith(
+ 'hello from prompt-interactive',
+ );
+ });
+ });
+});
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
new file mode 100644
index 00000000..ac3eb74d
--- /dev/null
+++ b/packages/cli/src/ui/App.tsx
@@ -0,0 +1,989 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
+import {
+ Box,
+ DOMElement,
+ measureElement,
+ Static,
+ Text,
+ useStdin,
+ useStdout,
+ useInput,
+ type Key as InkKeyType,
+} from 'ink';
+import { StreamingState, type HistoryItem, MessageType } from './types.js';
+import { useTerminalSize } from './hooks/useTerminalSize.js';
+import { useGeminiStream } from './hooks/useGeminiStream.js';
+import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
+import { useThemeCommand } from './hooks/useThemeCommand.js';
+import { useAuthCommand } from './hooks/useAuthCommand.js';
+import { useEditorSettings } from './hooks/useEditorSettings.js';
+import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
+import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
+import { useConsoleMessages } from './hooks/useConsoleMessages.js';
+import { Header } from './components/Header.js';
+import { LoadingIndicator } from './components/LoadingIndicator.js';
+import { AutoAcceptIndicator } from './components/AutoAcceptIndicator.js';
+import { ShellModeIndicator } from './components/ShellModeIndicator.js';
+import { InputPrompt } from './components/InputPrompt.js';
+import { Footer } from './components/Footer.js';
+import { ThemeDialog } from './components/ThemeDialog.js';
+import { AuthDialog } from './components/AuthDialog.js';
+import { AuthInProgress } from './components/AuthInProgress.js';
+import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
+import { Colors } from './colors.js';
+import { Help } from './components/Help.js';
+import { loadHierarchicalGeminiMemory } from '../config/config.js';
+import { LoadedSettings } from '../config/settings.js';
+import { Tips } from './components/Tips.js';
+import { ConsolePatcher } from './utils/ConsolePatcher.js';
+import { registerCleanup } from '../utils/cleanup.js';
+import { DetailedMessagesDisplay } from './components/DetailedMessagesDisplay.js';
+import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
+import { ContextSummaryDisplay } from './components/ContextSummaryDisplay.js';
+import { useHistory } from './hooks/useHistoryManager.js';
+import process from 'node:process';
+import {
+ getErrorMessage,
+ type Config,
+ getAllGeminiMdFilenames,
+ ApprovalMode,
+ isEditorAvailable,
+ EditorType,
+ FlashFallbackEvent,
+ logFlashFallback,
+} from '@qwen/qwen-code-core';
+import { validateAuthMethod } from '../config/auth.js';
+import { useLogger } from './hooks/useLogger.js';
+import { StreamingContext } from './contexts/StreamingContext.js';
+import {
+ SessionStatsProvider,
+ useSessionStats,
+} from './contexts/SessionContext.js';
+import { useGitBranchName } from './hooks/useGitBranchName.js';
+import { useBracketedPaste } from './hooks/useBracketedPaste.js';
+import { useTextBuffer } from './components/shared/text-buffer.js';
+import * as fs from 'fs';
+import { UpdateNotification } from './components/UpdateNotification.js';
+import {
+ isProQuotaExceededError,
+ isGenericQuotaExceededError,
+ UserTierId,
+} from '@qwen/qwen-code-core';
+import { checkForUpdates } from './utils/updateCheck.js';
+import ansiEscapes from 'ansi-escapes';
+import { OverflowProvider } from './contexts/OverflowContext.js';
+import { ShowMoreLines } from './components/ShowMoreLines.js';
+import { PrivacyNotice } from './privacy/PrivacyNotice.js';
+
+const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
+
+interface AppProps {
+ config: Config;
+ settings: LoadedSettings;
+ startupWarnings?: string[];
+ version: string;
+}
+
+export const AppWrapper = (props: AppProps) => (
+
+
+
+);
+
+const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
+ useBracketedPaste();
+ const [updateMessage, setUpdateMessage] = useState(null);
+ const { stdout } = useStdout();
+ const nightly = version.includes('nightly');
+
+ useEffect(() => {
+ checkForUpdates().then(setUpdateMessage);
+ }, []);
+
+ const { history, addItem, clearItems, loadHistory } = useHistory();
+ const {
+ consoleMessages,
+ handleNewMessage,
+ clearConsoleMessages: clearConsoleMessagesState,
+ } = useConsoleMessages();
+
+ useEffect(() => {
+ const consolePatcher = new ConsolePatcher({
+ onNewMessage: handleNewMessage,
+ debugMode: config.getDebugMode(),
+ });
+ consolePatcher.patch();
+ registerCleanup(consolePatcher.cleanup);
+ }, [handleNewMessage, config]);
+
+ const { stats: sessionStats } = useSessionStats();
+ const [staticNeedsRefresh, setStaticNeedsRefresh] = useState(false);
+ const [staticKey, setStaticKey] = useState(0);
+ const refreshStatic = useCallback(() => {
+ stdout.write(ansiEscapes.clearTerminal);
+ setStaticKey((prev) => prev + 1);
+ }, [setStaticKey, stdout]);
+
+ const [geminiMdFileCount, setGeminiMdFileCount] = useState(0);
+ const [debugMessage, setDebugMessage] = useState('');
+ const [showHelp, setShowHelp] = useState(false);
+ const [themeError, setThemeError] = useState(null);
+ const [authError, setAuthError] = useState(null);
+ const [editorError, setEditorError] = useState(null);
+ const [footerHeight, setFooterHeight] = useState(0);
+ const [corgiMode, setCorgiMode] = useState(false);
+ const [currentModel, setCurrentModel] = useState(config.getModel());
+ const [shellModeActive, setShellModeActive] = useState(false);
+ const [showErrorDetails, setShowErrorDetails] = useState(false);
+ const [showToolDescriptions, setShowToolDescriptions] =
+ useState(false);
+ const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
+ const [quittingMessages, setQuittingMessages] = useState<
+ HistoryItem[] | null
+ >(null);
+ const ctrlCTimerRef = useRef(null);
+ const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false);
+ const ctrlDTimerRef = useRef(null);
+ const [constrainHeight, setConstrainHeight] = useState(true);
+ const [showPrivacyNotice, setShowPrivacyNotice] = useState(false);
+ const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
+ useState(false);
+ const [userTier, setUserTier] = useState(undefined);
+
+ const openPrivacyNotice = useCallback(() => {
+ setShowPrivacyNotice(true);
+ }, []);
+ const initialPromptSubmitted = useRef(false);
+
+ const errorCount = useMemo(
+ () => consoleMessages.filter((msg) => msg.type === 'error').length,
+ [consoleMessages],
+ );
+
+ const {
+ isThemeDialogOpen,
+ openThemeDialog,
+ handleThemeSelect,
+ handleThemeHighlight,
+ } = useThemeCommand(settings, setThemeError, addItem);
+
+ const {
+ isAuthDialogOpen,
+ openAuthDialog,
+ handleAuthSelect,
+ isAuthenticating,
+ cancelAuthentication,
+ } = useAuthCommand(settings, setAuthError, config);
+
+ useEffect(() => {
+ if (settings.merged.selectedAuthType) {
+ const error = validateAuthMethod(settings.merged.selectedAuthType);
+ if (error) {
+ setAuthError(error);
+ openAuthDialog();
+ }
+ }
+ }, [settings.merged.selectedAuthType, openAuthDialog, setAuthError]);
+
+ // Sync user tier from config when authentication changes
+ useEffect(() => {
+ const syncUserTier = async () => {
+ try {
+ const configUserTier = await config.getUserTier();
+ if (configUserTier !== userTier) {
+ setUserTier(configUserTier);
+ }
+ } catch (error) {
+ // Silently fail - this is not critical functionality
+ // Only log in debug mode to avoid cluttering the console
+ if (config.getDebugMode()) {
+ console.debug('Failed to sync user tier:', error);
+ }
+ }
+ };
+
+ // Only sync when not currently authenticating
+ if (!isAuthenticating) {
+ syncUserTier();
+ }
+ }, [config, userTier, isAuthenticating]);
+
+ const {
+ isEditorDialogOpen,
+ openEditorDialog,
+ handleEditorSelect,
+ exitEditorDialog,
+ } = useEditorSettings(settings, setEditorError, addItem);
+
+ const toggleCorgiMode = useCallback(() => {
+ setCorgiMode((prev) => !prev);
+ }, []);
+
+ const performMemoryRefresh = useCallback(async () => {
+ addItem(
+ {
+ type: MessageType.INFO,
+ text: 'Refreshing hierarchical memory (QWEN.md or other context files)...',
+ },
+ Date.now(),
+ );
+ try {
+ const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
+ process.cwd(),
+ config.getDebugMode(),
+ config.getFileService(),
+ config.getExtensionContextFilePaths(),
+ );
+ config.setUserMemory(memoryContent);
+ config.setGeminiMdFileCount(fileCount);
+ setGeminiMdFileCount(fileCount);
+
+ addItem(
+ {
+ type: MessageType.INFO,
+ text: `Memory refreshed successfully. ${memoryContent.length > 0 ? `Loaded ${memoryContent.length} characters from ${fileCount} file(s).` : 'No memory content found.'}`,
+ },
+ Date.now(),
+ );
+ if (config.getDebugMode()) {
+ console.log(
+ `[DEBUG] Refreshed memory content in config: ${memoryContent.substring(0, 200)}...`,
+ );
+ }
+ } catch (error) {
+ const errorMessage = getErrorMessage(error);
+ addItem(
+ {
+ type: MessageType.ERROR,
+ text: `Error refreshing memory: ${errorMessage}`,
+ },
+ Date.now(),
+ );
+ console.error('Error refreshing memory:', error);
+ }
+ }, [config, addItem]);
+
+ // Watch for model changes (e.g., from Flash fallback)
+ useEffect(() => {
+ const checkModelChange = () => {
+ const configModel = config.getModel();
+ if (configModel !== currentModel) {
+ setCurrentModel(configModel);
+ }
+ };
+
+ // Check immediately and then periodically
+ checkModelChange();
+ const interval = setInterval(checkModelChange, 1000); // Check every second
+
+ return () => clearInterval(interval);
+ }, [config, currentModel]);
+
+ // Set up Flash fallback handler
+ useEffect(() => {
+ const flashFallbackHandler = async (
+ currentModel: string,
+ fallbackModel: string,
+ error?: unknown,
+ ): Promise => {
+ let message: string;
+
+ // Use actual user tier if available, otherwise default to FREE tier behavior (safe default)
+ const isPaidTier =
+ userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
+
+ // Check if this is a Pro quota exceeded error
+ if (error && isProQuotaExceededError(error)) {
+ if (isPaidTier) {
+ message = `โก You have reached your daily ${currentModel} quota limit.
+โก Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session.
+โก To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
+ } else {
+ message = `โก You have reached your daily ${currentModel} quota limit.
+โก Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session.
+โก To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist
+โก Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
+โก You can switch authentication methods by typing /auth`;
+ }
+ } else if (error && isGenericQuotaExceededError(error)) {
+ if (isPaidTier) {
+ message = `โก You have reached your daily quota limit.
+โก Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session.
+โก To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
+ } else {
+ message = `โก You have reached your daily quota limit.
+โก Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session.
+โก To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist
+โก Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
+โก You can switch authentication methods by typing /auth`;
+ }
+ } else {
+ if (isPaidTier) {
+ // Default fallback message for other cases (like consecutive 429s)
+ message = `โก Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.
+โก Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${currentModel} quota limit
+โก To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
+ } else {
+ // Default fallback message for other cases (like consecutive 429s)
+ message = `โก Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.
+โก Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${currentModel} quota limit
+โก To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist
+โก Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
+โก You can switch authentication methods by typing /auth`;
+ }
+ }
+
+ // Add message to UI history
+ addItem(
+ {
+ type: MessageType.INFO,
+ text: message,
+ },
+ Date.now(),
+ );
+
+ // Set the flag to prevent tool continuation
+ setModelSwitchedFromQuotaError(true);
+ // Set global quota error flag to prevent Flash model calls
+ config.setQuotaErrorOccurred(true);
+ // Switch model for future use but return false to stop current retry
+ config.setModel(fallbackModel);
+ logFlashFallback(
+ config,
+ new FlashFallbackEvent(config.getContentGeneratorConfig().authType!),
+ );
+ return false; // Don't continue with current prompt
+ };
+
+ config.setFlashFallbackHandler(flashFallbackHandler);
+ }, [config, addItem, userTier]);
+
+ const {
+ handleSlashCommand,
+ slashCommands,
+ pendingHistoryItems: pendingSlashCommandHistoryItems,
+ commandContext,
+ } = useSlashCommandProcessor(
+ config,
+ settings,
+ history,
+ addItem,
+ clearItems,
+ loadHistory,
+ refreshStatic,
+ setShowHelp,
+ setDebugMessage,
+ openThemeDialog,
+ openAuthDialog,
+ openEditorDialog,
+ toggleCorgiMode,
+ showToolDescriptions,
+ setQuittingMessages,
+ openPrivacyNotice,
+ );
+ const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
+
+ const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
+ const isInitialMount = useRef(true);
+ const { stdin, setRawMode } = useStdin();
+ const isValidPath = useCallback((filePath: string): boolean => {
+ try {
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
+ } catch (_e) {
+ return false;
+ }
+ }, []);
+
+ const widthFraction = 0.9;
+ const inputWidth = Math.max(
+ 20,
+ Math.floor(terminalWidth * widthFraction) - 3,
+ );
+ const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8));
+
+ const buffer = useTextBuffer({
+ initialText: '',
+ viewport: { height: 10, width: inputWidth },
+ stdin,
+ setRawMode,
+ isValidPath,
+ shellModeActive,
+ });
+
+ const handleExit = useCallback(
+ (
+ pressedOnce: boolean,
+ setPressedOnce: (value: boolean) => void,
+ timerRef: React.MutableRefObject,
+ ) => {
+ if (pressedOnce) {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ const quitCommand = slashCommands.find(
+ (cmd) => cmd.name === 'quit' || cmd.altName === 'exit',
+ );
+ if (quitCommand && quitCommand.action) {
+ quitCommand.action(commandContext, '');
+ } else {
+ // This is unlikely to be needed but added for an additional fallback.
+ process.exit(0);
+ }
+ } else {
+ setPressedOnce(true);
+ timerRef.current = setTimeout(() => {
+ setPressedOnce(false);
+ timerRef.current = null;
+ }, CTRL_EXIT_PROMPT_DURATION_MS);
+ }
+ },
+ // Add commandContext to the dependency array here!
+ [slashCommands, commandContext],
+ );
+
+ useInput((input: string, key: InkKeyType) => {
+ let enteringConstrainHeightMode = false;
+ if (!constrainHeight) {
+ // Automatically re-enter constrain height mode if the user types
+ // anything. When constrainHeight==false, the user will experience
+ // significant flickering so it is best to disable it immediately when
+ // the user starts interacting with the app.
+ enteringConstrainHeightMode = true;
+ setConstrainHeight(true);
+ }
+
+ if (key.ctrl && input === 'o') {
+ setShowErrorDetails((prev) => !prev);
+ } else if (key.ctrl && input === 't') {
+ const newValue = !showToolDescriptions;
+ setShowToolDescriptions(newValue);
+
+ const mcpServers = config.getMcpServers();
+ if (Object.keys(mcpServers || {}).length > 0) {
+ handleSlashCommand(newValue ? '/mcp desc' : '/mcp nodesc');
+ }
+ } else if (key.ctrl && (input === 'c' || input === 'C')) {
+ handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef);
+ } else if (key.ctrl && (input === 'd' || input === 'D')) {
+ if (buffer.text.length > 0) {
+ // Do nothing if there is text in the input.
+ return;
+ }
+ handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef);
+ } else if (key.ctrl && input === 's' && !enteringConstrainHeightMode) {
+ setConstrainHeight(false);
+ }
+ });
+
+ useEffect(() => {
+ if (config) {
+ setGeminiMdFileCount(config.getGeminiMdFileCount());
+ }
+ }, [config]);
+
+ const getPreferredEditor = useCallback(() => {
+ const editorType = settings.merged.preferredEditor;
+ const isValidEditor = isEditorAvailable(editorType);
+ if (!isValidEditor) {
+ openEditorDialog();
+ return;
+ }
+ return editorType as EditorType;
+ }, [settings, openEditorDialog]);
+
+ const onAuthError = useCallback(() => {
+ setAuthError('reauth required');
+ openAuthDialog();
+ }, [openAuthDialog, setAuthError]);
+
+ const {
+ streamingState,
+ submitQuery,
+ initError,
+ pendingHistoryItems: pendingGeminiHistoryItems,
+ thought,
+ } = useGeminiStream(
+ config.getGeminiClient(),
+ history,
+ addItem,
+ setShowHelp,
+ config,
+ setDebugMessage,
+ handleSlashCommand,
+ shellModeActive,
+ getPreferredEditor,
+ onAuthError,
+ performMemoryRefresh,
+ modelSwitchedFromQuotaError,
+ setModelSwitchedFromQuotaError,
+ );
+ pendingHistoryItems.push(...pendingGeminiHistoryItems);
+ const { elapsedTime, currentLoadingPhrase } =
+ useLoadingIndicator(streamingState);
+ const showAutoAcceptIndicator = useAutoAcceptIndicator({ config });
+
+ const handleFinalSubmit = useCallback(
+ (submittedValue: string) => {
+ const trimmedValue = submittedValue.trim();
+ if (trimmedValue.length > 0) {
+ submitQuery(trimmedValue);
+ }
+ },
+ [submitQuery],
+ );
+
+ const logger = useLogger();
+ const [userMessages, setUserMessages] = useState([]);
+
+ useEffect(() => {
+ const fetchUserMessages = async () => {
+ const pastMessagesRaw = (await logger?.getPreviousUserMessages()) || []; // Newest first
+
+ const currentSessionUserMessages = history
+ .filter(
+ (item): item is HistoryItem & { type: 'user'; text: string } =>
+ item.type === 'user' &&
+ typeof item.text === 'string' &&
+ item.text.trim() !== '',
+ )
+ .map((item) => item.text)
+ .reverse(); // Newest first, to match pastMessagesRaw sorting
+
+ // Combine, with current session messages being more recent
+ const combinedMessages = [
+ ...currentSessionUserMessages,
+ ...pastMessagesRaw,
+ ];
+
+ // Deduplicate consecutive identical messages from the combined list (still newest first)
+ const deduplicatedMessages: string[] = [];
+ if (combinedMessages.length > 0) {
+ deduplicatedMessages.push(combinedMessages[0]); // Add the newest one unconditionally
+ for (let i = 1; i < combinedMessages.length; i++) {
+ if (combinedMessages[i] !== combinedMessages[i - 1]) {
+ deduplicatedMessages.push(combinedMessages[i]);
+ }
+ }
+ }
+ // Reverse to oldest first for useInputHistory
+ setUserMessages(deduplicatedMessages.reverse());
+ };
+ fetchUserMessages();
+ }, [history, logger]);
+
+ const isInputActive = streamingState === StreamingState.Idle && !initError;
+
+ const handleClearScreen = useCallback(() => {
+ clearItems();
+ clearConsoleMessagesState();
+ console.clear();
+ refreshStatic();
+ }, [clearItems, clearConsoleMessagesState, refreshStatic]);
+
+ const mainControlsRef = useRef(null);
+ const pendingHistoryItemRef = useRef(null);
+
+ useEffect(() => {
+ if (mainControlsRef.current) {
+ const fullFooterMeasurement = measureElement(mainControlsRef.current);
+ setFooterHeight(fullFooterMeasurement.height);
+ }
+ }, [terminalHeight, consoleMessages, showErrorDetails]);
+
+ const staticExtraHeight = /* margins and padding */ 3;
+ const availableTerminalHeight = useMemo(
+ () => terminalHeight - footerHeight - staticExtraHeight,
+ [terminalHeight, footerHeight],
+ );
+
+ useEffect(() => {
+ // skip refreshing Static during first mount
+ if (isInitialMount.current) {
+ isInitialMount.current = false;
+ return;
+ }
+
+ // debounce so it doesn't fire up too often during resize
+ const handler = setTimeout(() => {
+ setStaticNeedsRefresh(false);
+ refreshStatic();
+ }, 300);
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [terminalWidth, terminalHeight, refreshStatic]);
+
+ useEffect(() => {
+ if (streamingState === StreamingState.Idle && staticNeedsRefresh) {
+ setStaticNeedsRefresh(false);
+ refreshStatic();
+ }
+ }, [streamingState, refreshStatic, staticNeedsRefresh]);
+
+ const filteredConsoleMessages = useMemo(() => {
+ if (config.getDebugMode()) {
+ return consoleMessages;
+ }
+ return consoleMessages.filter((msg) => msg.type !== 'debug');
+ }, [consoleMessages, config]);
+
+ const branchName = useGitBranchName(config.getTargetDir());
+
+ const contextFileNames = useMemo(() => {
+ const fromSettings = settings.merged.contextFileName;
+ if (fromSettings) {
+ return Array.isArray(fromSettings) ? fromSettings : [fromSettings];
+ }
+ return getAllGeminiMdFilenames();
+ }, [settings.merged.contextFileName]);
+
+ const initialPrompt = useMemo(() => config.getQuestion(), [config]);
+ const geminiClient = config.getGeminiClient();
+
+ useEffect(() => {
+ if (
+ initialPrompt &&
+ !initialPromptSubmitted.current &&
+ !isAuthenticating &&
+ !isAuthDialogOpen &&
+ !isThemeDialogOpen &&
+ !isEditorDialogOpen &&
+ !showPrivacyNotice &&
+ geminiClient?.isInitialized?.()
+ ) {
+ submitQuery(initialPrompt);
+ initialPromptSubmitted.current = true;
+ }
+ }, [
+ initialPrompt,
+ submitQuery,
+ isAuthenticating,
+ isAuthDialogOpen,
+ isThemeDialogOpen,
+ isEditorDialogOpen,
+ showPrivacyNotice,
+ geminiClient,
+ ]);
+
+ if (quittingMessages) {
+ return (
+
+ {quittingMessages.map((item) => (
+
+ ))}
+
+ );
+ }
+ const mainAreaWidth = Math.floor(terminalWidth * 0.9);
+ const debugConsoleMaxHeight = Math.floor(Math.max(terminalHeight * 0.2, 5));
+ // Arbitrary threshold to ensure that items in the static area are large
+ // enough but not too large to make the terminal hard to use.
+ const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100);
+ return (
+
+
+ {/* Move UpdateNotification outside Static so it can re-render when updateMessage changes */}
+ {updateMessage && }
+
+ {/*
+ * The Static component is an Ink intrinsic in which there can only be 1 per application.
+ * Because of this restriction we're hacking it slightly by having a 'header' item here to
+ * ensure that it's statically rendered.
+ *
+ * Background on the Static Item: Anything in the Static component is written a single time
+ * to the console. Think of it like doing a console.log and then never using ANSI codes to
+ * clear that content ever again. Effectively it has a moving frame that every time new static
+ * content is set it'll flush content to the terminal and move the area which it's "clearing"
+ * down a notch. Without Static the area which gets erased and redrawn continuously grows.
+ */}
+
+ {!settings.merged.hideBanner && (
+
+ )}
+ {!settings.merged.hideTips && }
+ ,
+ ...history.map((h) => (
+
+ )),
+ ]}
+ >
+ {(item) => item}
+
+
+
+ {pendingHistoryItems.map((item, i) => (
+
+ ))}
+
+
+
+
+ {showHelp && }
+
+
+ {startupWarnings.length > 0 && (
+
+ {startupWarnings.map((warning, index) => (
+
+ {warning}
+
+ ))}
+
+ )}
+
+ {isThemeDialogOpen ? (
+
+ {themeError && (
+
+ {themeError}
+
+ )}
+
+
+ ) : isAuthenticating ? (
+ <>
+ {
+ setAuthError('Authentication timed out. Please try again.');
+ cancelAuthentication();
+ openAuthDialog();
+ }}
+ />
+ {showErrorDetails && (
+
+
+
+
+
+
+ )}
+ >
+ ) : isAuthDialogOpen ? (
+
+
+
+ ) : isEditorDialogOpen ? (
+
+ {editorError && (
+
+ {editorError}
+
+ )}
+
+
+ ) : showPrivacyNotice ? (
+ setShowPrivacyNotice(false)}
+ config={config}
+ />
+ ) : (
+ <>
+
+
+
+ {process.env.GEMINI_SYSTEM_MD && (
+ |โโ _โ |
+ )}
+ {ctrlCPressedOnce ? (
+
+ Press Ctrl+C again to exit.
+
+ ) : ctrlDPressedOnce ? (
+
+ Press Ctrl+D again to exit.
+
+ ) : (
+
+ )}
+
+
+ {showAutoAcceptIndicator !== ApprovalMode.DEFAULT &&
+ !shellModeActive && (
+
+ )}
+ {shellModeActive && }
+
+
+
+ {showErrorDetails && (
+
+
+
+
+
+
+ )}
+
+ {isInputActive && (
+
+ )}
+ >
+ )}
+
+ {initError && streamingState !== StreamingState.Responding && (
+
+ {history.find(
+ (item) =>
+ item.type === 'error' && item.text?.includes(initError),
+ )?.text ? (
+
+ {
+ history.find(
+ (item) =>
+ item.type === 'error' && item.text?.includes(initError),
+ )?.text
+ }
+
+ ) : (
+ <>
+
+ Initialization Error: {initError}
+
+
+ {' '}
+ Please check API key and configuration.
+
+ >
+ )}
+
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/colors.ts b/packages/cli/src/ui/colors.ts
new file mode 100644
index 00000000..bb8451cc
--- /dev/null
+++ b/packages/cli/src/ui/colors.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { themeManager } from './themes/theme-manager.js';
+import { ColorsTheme } from './themes/theme.js';
+
+export const Colors: ColorsTheme = {
+ get type() {
+ return themeManager.getActiveTheme().colors.type;
+ },
+ get Foreground() {
+ return themeManager.getActiveTheme().colors.Foreground;
+ },
+ get Background() {
+ return themeManager.getActiveTheme().colors.Background;
+ },
+ get LightBlue() {
+ return themeManager.getActiveTheme().colors.LightBlue;
+ },
+ get AccentBlue() {
+ return themeManager.getActiveTheme().colors.AccentBlue;
+ },
+ get AccentPurple() {
+ return themeManager.getActiveTheme().colors.AccentPurple;
+ },
+ get AccentCyan() {
+ return themeManager.getActiveTheme().colors.AccentCyan;
+ },
+ get AccentGreen() {
+ return themeManager.getActiveTheme().colors.AccentGreen;
+ },
+ get AccentYellow() {
+ return themeManager.getActiveTheme().colors.AccentYellow;
+ },
+ get AccentRed() {
+ return themeManager.getActiveTheme().colors.AccentRed;
+ },
+ get Comment() {
+ return themeManager.getActiveTheme().colors.Comment;
+ },
+ get Gray() {
+ return themeManager.getActiveTheme().colors.Gray;
+ },
+ get GradientColors() {
+ return themeManager.getActiveTheme().colors.GradientColors;
+ },
+};
diff --git a/packages/cli/src/ui/commands/aboutCommand.test.ts b/packages/cli/src/ui/commands/aboutCommand.test.ts
new file mode 100644
index 00000000..48dd6db3
--- /dev/null
+++ b/packages/cli/src/ui/commands/aboutCommand.test.ts
@@ -0,0 +1,117 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { aboutCommand } from './aboutCommand.js';
+import { type CommandContext } from './types.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+import * as versionUtils from '../../utils/version.js';
+import { MessageType } from '../types.js';
+
+vi.mock('../../utils/version.js', () => ({
+ getCliVersion: vi.fn(),
+}));
+
+describe('aboutCommand', () => {
+ let mockContext: CommandContext;
+ const originalPlatform = process.platform;
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getModel: vi.fn(),
+ },
+ settings: {
+ merged: {
+ selectedAuthType: 'test-auth',
+ },
+ },
+ },
+ ui: {
+ addItem: vi.fn(),
+ },
+ } as unknown as CommandContext);
+
+ vi.mocked(versionUtils.getCliVersion).mockResolvedValue('test-version');
+ vi.spyOn(mockContext.services.config!, 'getModel').mockReturnValue(
+ 'test-model',
+ );
+ process.env.GOOGLE_CLOUD_PROJECT = 'test-gcp-project';
+ Object.defineProperty(process, 'platform', {
+ value: 'test-os',
+ });
+ });
+
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ Object.defineProperty(process, 'platform', {
+ value: originalPlatform,
+ });
+ process.env = originalEnv;
+ vi.clearAllMocks();
+ });
+
+ it('should have the correct name and description', () => {
+ expect(aboutCommand.name).toBe('about');
+ expect(aboutCommand.description).toBe('show version info');
+ });
+
+ it('should call addItem with all version info', async () => {
+ if (!aboutCommand.action) {
+ throw new Error('The about command must have an action.');
+ }
+
+ await aboutCommand.action(mockContext, '');
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.ABOUT,
+ cliVersion: 'test-version',
+ osVersion: 'test-os',
+ sandboxEnv: 'no sandbox',
+ modelVersion: 'test-model',
+ selectedAuthType: 'test-auth',
+ gcpProject: 'test-gcp-project',
+ },
+ expect.any(Number),
+ );
+ });
+
+ it('should show the correct sandbox environment variable', async () => {
+ process.env.SANDBOX = 'gemini-sandbox';
+ if (!aboutCommand.action) {
+ throw new Error('The about command must have an action.');
+ }
+
+ await aboutCommand.action(mockContext, '');
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sandboxEnv: 'gemini-sandbox',
+ }),
+ expect.any(Number),
+ );
+ });
+
+ it('should show sandbox-exec profile when applicable', async () => {
+ process.env.SANDBOX = 'sandbox-exec';
+ process.env.SEATBELT_PROFILE = 'test-profile';
+ if (!aboutCommand.action) {
+ throw new Error('The about command must have an action.');
+ }
+
+ await aboutCommand.action(mockContext, '');
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sandboxEnv: 'sandbox-exec (test-profile)',
+ }),
+ expect.any(Number),
+ );
+ });
+});
diff --git a/packages/cli/src/ui/commands/aboutCommand.ts b/packages/cli/src/ui/commands/aboutCommand.ts
new file mode 100644
index 00000000..3cb8c2f6
--- /dev/null
+++ b/packages/cli/src/ui/commands/aboutCommand.ts
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { getCliVersion } from '../../utils/version.js';
+import { SlashCommand } from './types.js';
+import process from 'node:process';
+import { MessageType, type HistoryItemAbout } from '../types.js';
+
+export const aboutCommand: SlashCommand = {
+ name: 'about',
+ description: 'show version info',
+ action: async (context) => {
+ const osVersion = process.platform;
+ let sandboxEnv = 'no sandbox';
+ if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
+ sandboxEnv = process.env.SANDBOX;
+ } else if (process.env.SANDBOX === 'sandbox-exec') {
+ sandboxEnv = `sandbox-exec (${
+ process.env.SEATBELT_PROFILE || 'unknown'
+ })`;
+ }
+ const modelVersion = context.services.config?.getModel() || 'Unknown';
+ const cliVersion = await getCliVersion();
+ const selectedAuthType =
+ context.services.settings.merged.selectedAuthType || '';
+ const gcpProject = process.env.GOOGLE_CLOUD_PROJECT || '';
+
+ const aboutItem: Omit = {
+ type: MessageType.ABOUT,
+ cliVersion,
+ osVersion,
+ sandboxEnv,
+ modelVersion,
+ selectedAuthType,
+ gcpProject,
+ };
+
+ context.ui.addItem(aboutItem, Date.now());
+ },
+};
diff --git a/packages/cli/src/ui/commands/authCommand.test.ts b/packages/cli/src/ui/commands/authCommand.test.ts
new file mode 100644
index 00000000..d6d925db
--- /dev/null
+++ b/packages/cli/src/ui/commands/authCommand.test.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach } from 'vitest';
+import { authCommand } from './authCommand.js';
+import { type CommandContext } from './types.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+
+describe('authCommand', () => {
+ let mockContext: CommandContext;
+
+ beforeEach(() => {
+ mockContext = createMockCommandContext();
+ });
+
+ it('should return a dialog action to open the auth dialog', () => {
+ if (!authCommand.action) {
+ throw new Error('The auth command must have an action.');
+ }
+
+ const result = authCommand.action(mockContext, '');
+
+ expect(result).toEqual({
+ type: 'dialog',
+ dialog: 'auth',
+ });
+ });
+
+ it('should have the correct name and description', () => {
+ expect(authCommand.name).toBe('auth');
+ expect(authCommand.description).toBe('change the auth method');
+ });
+});
diff --git a/packages/cli/src/ui/commands/authCommand.ts b/packages/cli/src/ui/commands/authCommand.ts
new file mode 100644
index 00000000..29bd2c9d
--- /dev/null
+++ b/packages/cli/src/ui/commands/authCommand.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { OpenDialogActionReturn, SlashCommand } from './types.js';
+
+export const authCommand: SlashCommand = {
+ name: 'auth',
+ description: 'change the auth method',
+ action: (_context, _args): OpenDialogActionReturn => ({
+ type: 'dialog',
+ dialog: 'auth',
+ }),
+};
diff --git a/packages/cli/src/ui/commands/clearCommand.test.ts b/packages/cli/src/ui/commands/clearCommand.test.ts
new file mode 100644
index 00000000..8019dd68
--- /dev/null
+++ b/packages/cli/src/ui/commands/clearCommand.test.ts
@@ -0,0 +1,78 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach, Mock } from 'vitest';
+import { clearCommand } from './clearCommand.js';
+import { type CommandContext } from './types.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+import { GeminiClient } from '@google/gemini-cli-core';
+
+describe('clearCommand', () => {
+ let mockContext: CommandContext;
+ let mockResetChat: ReturnType;
+
+ beforeEach(() => {
+ mockResetChat = vi.fn().mockResolvedValue(undefined);
+
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getGeminiClient: () =>
+ ({
+ resetChat: mockResetChat,
+ }) as unknown as GeminiClient,
+ },
+ },
+ });
+ });
+
+ it('should set debug message, reset chat, and clear UI when config is available', async () => {
+ if (!clearCommand.action) {
+ throw new Error('clearCommand must have an action.');
+ }
+
+ await clearCommand.action(mockContext, '');
+
+ expect(mockContext.ui.setDebugMessage).toHaveBeenCalledWith(
+ 'Clearing terminal and resetting chat.',
+ );
+ expect(mockContext.ui.setDebugMessage).toHaveBeenCalledTimes(1);
+
+ expect(mockResetChat).toHaveBeenCalledTimes(1);
+
+ expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
+
+ // Check the order of operations.
+ const setDebugMessageOrder = (mockContext.ui.setDebugMessage as Mock).mock
+ .invocationCallOrder[0];
+ const resetChatOrder = mockResetChat.mock.invocationCallOrder[0];
+ const clearOrder = (mockContext.ui.clear as Mock).mock
+ .invocationCallOrder[0];
+
+ expect(setDebugMessageOrder).toBeLessThan(resetChatOrder);
+ expect(resetChatOrder).toBeLessThan(clearOrder);
+ });
+
+ it('should not attempt to reset chat if config service is not available', async () => {
+ if (!clearCommand.action) {
+ throw new Error('clearCommand must have an action.');
+ }
+
+ const nullConfigContext = createMockCommandContext({
+ services: {
+ config: null,
+ },
+ });
+
+ await clearCommand.action(nullConfigContext, '');
+
+ expect(nullConfigContext.ui.setDebugMessage).toHaveBeenCalledWith(
+ 'Clearing terminal and resetting chat.',
+ );
+ expect(mockResetChat).not.toHaveBeenCalled();
+ expect(nullConfigContext.ui.clear).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/cli/src/ui/commands/clearCommand.ts b/packages/cli/src/ui/commands/clearCommand.ts
new file mode 100644
index 00000000..e5473b5b
--- /dev/null
+++ b/packages/cli/src/ui/commands/clearCommand.ts
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SlashCommand } from './types.js';
+
+export const clearCommand: SlashCommand = {
+ name: 'clear',
+ description: 'clear the screen and conversation history',
+ action: async (context, _args) => {
+ context.ui.setDebugMessage('Clearing terminal and resetting chat.');
+ await context.services.config?.getGeminiClient()?.resetChat();
+ context.ui.clear();
+ },
+};
diff --git a/packages/cli/src/ui/commands/helpCommand.test.ts b/packages/cli/src/ui/commands/helpCommand.test.ts
new file mode 100644
index 00000000..a6b19c05
--- /dev/null
+++ b/packages/cli/src/ui/commands/helpCommand.test.ts
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach } from 'vitest';
+import { helpCommand } from './helpCommand.js';
+import { type CommandContext } from './types.js';
+
+describe('helpCommand', () => {
+ let mockContext: CommandContext;
+
+ beforeEach(() => {
+ mockContext = {} as unknown as CommandContext;
+ });
+
+ it("should return a dialog action and log a debug message for '/help'", () => {
+ const consoleDebugSpy = vi
+ .spyOn(console, 'debug')
+ .mockImplementation(() => {});
+ if (!helpCommand.action) {
+ throw new Error('Help command has no action');
+ }
+ const result = helpCommand.action(mockContext, '');
+
+ expect(result).toEqual({
+ type: 'dialog',
+ dialog: 'help',
+ });
+ expect(consoleDebugSpy).toHaveBeenCalledWith('Opening help UI ...');
+ });
+
+ it("should also be triggered by its alternative name '?'", () => {
+ // This test is more conceptual. The routing of altName to the command
+ // is handled by the slash command processor, but we can assert the
+ // altName is correctly defined on the command object itself.
+ expect(helpCommand.altName).toBe('?');
+ });
+});
diff --git a/packages/cli/src/ui/commands/helpCommand.ts b/packages/cli/src/ui/commands/helpCommand.ts
new file mode 100644
index 00000000..6612626f
--- /dev/null
+++ b/packages/cli/src/ui/commands/helpCommand.ts
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { OpenDialogActionReturn, SlashCommand } from './types.js';
+
+export const helpCommand: SlashCommand = {
+ name: 'help',
+ altName: '?',
+ description: 'for help on qwen code',
+ action: (_context, _args): OpenDialogActionReturn => {
+ console.debug('Opening help UI ...');
+ return {
+ type: 'dialog',
+ dialog: 'help',
+ };
+ },
+};
diff --git a/packages/cli/src/ui/commands/memoryCommand.test.ts b/packages/cli/src/ui/commands/memoryCommand.test.ts
new file mode 100644
index 00000000..4d99fa06
--- /dev/null
+++ b/packages/cli/src/ui/commands/memoryCommand.test.ts
@@ -0,0 +1,248 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach, Mock } from 'vitest';
+import { memoryCommand } from './memoryCommand.js';
+import { type CommandContext, SlashCommand } from './types.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+import { MessageType } from '../types.js';
+import { getErrorMessage } from '@qwen/qwen-code-core';
+
+vi.mock('@qwen/qwen-code-core', async (importOriginal) => {
+ const original = await importOriginal();
+ return {
+ ...original,
+ getErrorMessage: vi.fn((error: unknown) => {
+ if (error instanceof Error) return error.message;
+ return String(error);
+ }),
+ };
+});
+
+describe('memoryCommand', () => {
+ let mockContext: CommandContext;
+
+ const getSubCommand = (name: 'show' | 'add' | 'refresh'): SlashCommand => {
+ const subCommand = memoryCommand.subCommands?.find(
+ (cmd) => cmd.name === name,
+ );
+ if (!subCommand) {
+ throw new Error(`/memory ${name} command not found.`);
+ }
+ return subCommand;
+ };
+
+ describe('/memory show', () => {
+ let showCommand: SlashCommand;
+ let mockGetUserMemory: Mock;
+ let mockGetGeminiMdFileCount: Mock;
+
+ beforeEach(() => {
+ showCommand = getSubCommand('show');
+
+ mockGetUserMemory = vi.fn();
+ mockGetGeminiMdFileCount = vi.fn();
+
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getUserMemory: mockGetUserMemory,
+ getGeminiMdFileCount: mockGetGeminiMdFileCount,
+ },
+ },
+ });
+ });
+
+ it('should display a message if memory is empty', async () => {
+ if (!showCommand.action) throw new Error('Command has no action');
+
+ mockGetUserMemory.mockReturnValue('');
+ mockGetGeminiMdFileCount.mockReturnValue(0);
+
+ await showCommand.action(mockContext, '');
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: 'Memory is currently empty.',
+ },
+ expect.any(Number),
+ );
+ });
+
+ it('should display the memory content and file count if it exists', async () => {
+ if (!showCommand.action) throw new Error('Command has no action');
+
+ const memoryContent = 'This is a test memory.';
+
+ mockGetUserMemory.mockReturnValue(memoryContent);
+ mockGetGeminiMdFileCount.mockReturnValue(1);
+
+ await showCommand.action(mockContext, '');
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: `Current memory content from 1 file(s):\n\n---\n${memoryContent}\n---`,
+ },
+ expect.any(Number),
+ );
+ });
+ });
+
+ describe('/memory add', () => {
+ let addCommand: SlashCommand;
+
+ beforeEach(() => {
+ addCommand = getSubCommand('add');
+ mockContext = createMockCommandContext();
+ });
+
+ it('should return an error message if no arguments are provided', () => {
+ if (!addCommand.action) throw new Error('Command has no action');
+
+ const result = addCommand.action(mockContext, ' ');
+ expect(result).toEqual({
+ type: 'message',
+ messageType: 'error',
+ content: 'Usage: /memory add ',
+ });
+
+ expect(mockContext.ui.addItem).not.toHaveBeenCalled();
+ });
+
+ it('should return a tool action and add an info message when arguments are provided', () => {
+ if (!addCommand.action) throw new Error('Command has no action');
+
+ const fact = 'remember this';
+ const result = addCommand.action(mockContext, ` ${fact} `);
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: `Attempting to save to memory: "${fact}"`,
+ },
+ expect.any(Number),
+ );
+
+ expect(result).toEqual({
+ type: 'tool',
+ toolName: 'save_memory',
+ toolArgs: { fact },
+ });
+ });
+ });
+
+ describe('/memory refresh', () => {
+ let refreshCommand: SlashCommand;
+ let mockRefreshMemory: Mock;
+
+ beforeEach(() => {
+ refreshCommand = getSubCommand('refresh');
+ mockRefreshMemory = vi.fn();
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ refreshMemory: mockRefreshMemory,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any,
+ },
+ });
+ });
+
+ it('should display success message when memory is refreshed with content', async () => {
+ if (!refreshCommand.action) throw new Error('Command has no action');
+
+ const refreshResult = {
+ memoryContent: 'new memory content',
+ fileCount: 2,
+ };
+ mockRefreshMemory.mockResolvedValue(refreshResult);
+
+ await refreshCommand.action(mockContext, '');
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: 'Refreshing memory from source files...',
+ },
+ expect.any(Number),
+ );
+
+ expect(mockRefreshMemory).toHaveBeenCalledOnce();
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: 'Memory refreshed successfully. Loaded 18 characters from 2 file(s).',
+ },
+ expect.any(Number),
+ );
+ });
+
+ it('should display success message when memory is refreshed with no content', async () => {
+ if (!refreshCommand.action) throw new Error('Command has no action');
+
+ const refreshResult = { memoryContent: '', fileCount: 0 };
+ mockRefreshMemory.mockResolvedValue(refreshResult);
+
+ await refreshCommand.action(mockContext, '');
+
+ expect(mockRefreshMemory).toHaveBeenCalledOnce();
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: 'Memory refreshed successfully. No memory content found.',
+ },
+ expect.any(Number),
+ );
+ });
+
+ it('should display an error message if refreshing fails', async () => {
+ if (!refreshCommand.action) throw new Error('Command has no action');
+
+ const error = new Error('Failed to read memory files.');
+ mockRefreshMemory.mockRejectedValue(error);
+
+ await refreshCommand.action(mockContext, '');
+
+ expect(mockRefreshMemory).toHaveBeenCalledOnce();
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.ERROR,
+ text: `Error refreshing memory: ${error.message}`,
+ },
+ expect.any(Number),
+ );
+
+ expect(getErrorMessage).toHaveBeenCalledWith(error);
+ });
+
+ it('should not throw if config service is unavailable', async () => {
+ if (!refreshCommand.action) throw new Error('Command has no action');
+
+ const nullConfigContext = createMockCommandContext({
+ services: { config: null },
+ });
+
+ await expect(
+ refreshCommand.action(nullConfigContext, ''),
+ ).resolves.toBeUndefined();
+
+ expect(nullConfigContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: 'Refreshing memory from source files...',
+ },
+ expect.any(Number),
+ );
+
+ expect(mockRefreshMemory).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/cli/src/ui/commands/memoryCommand.ts b/packages/cli/src/ui/commands/memoryCommand.ts
new file mode 100644
index 00000000..d6974040
--- /dev/null
+++ b/packages/cli/src/ui/commands/memoryCommand.ts
@@ -0,0 +1,106 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { getErrorMessage } from '@qwen/qwen-code-core';
+import { MessageType } from '../types.js';
+import { SlashCommand, SlashCommandActionReturn } from './types.js';
+
+export const memoryCommand: SlashCommand = {
+ name: 'memory',
+ description: 'Commands for interacting with memory.',
+ subCommands: [
+ {
+ name: 'show',
+ description: 'Show the current memory contents.',
+ action: async (context) => {
+ const memoryContent = context.services.config?.getUserMemory() || '';
+ const fileCount = context.services.config?.getGeminiMdFileCount() || 0;
+
+ const messageContent =
+ memoryContent.length > 0
+ ? `Current memory content from ${fileCount} file(s):\n\n---\n${memoryContent}\n---`
+ : 'Memory is currently empty.';
+
+ context.ui.addItem(
+ {
+ type: MessageType.INFO,
+ text: messageContent,
+ },
+ Date.now(),
+ );
+ },
+ },
+ {
+ name: 'add',
+ description: 'Add content to the memory.',
+ action: (context, args): SlashCommandActionReturn | void => {
+ if (!args || args.trim() === '') {
+ return {
+ type: 'message',
+ messageType: 'error',
+ content: 'Usage: /memory add ',
+ };
+ }
+
+ context.ui.addItem(
+ {
+ type: MessageType.INFO,
+ text: `Attempting to save to memory: "${args.trim()}"`,
+ },
+ Date.now(),
+ );
+
+ return {
+ type: 'tool',
+ toolName: 'save_memory',
+ toolArgs: { fact: args.trim() },
+ };
+ },
+ },
+ {
+ name: 'refresh',
+ description: 'Refresh the memory from the source.',
+ action: async (context) => {
+ context.ui.addItem(
+ {
+ type: MessageType.INFO,
+ text: 'Refreshing memory from source files...',
+ },
+ Date.now(),
+ );
+
+ try {
+ const result = await context.services.config?.refreshMemory();
+
+ if (result) {
+ const { memoryContent, fileCount } = result;
+ const successMessage =
+ memoryContent.length > 0
+ ? `Memory refreshed successfully. Loaded ${memoryContent.length} characters from ${fileCount} file(s).`
+ : 'Memory refreshed successfully. No memory content found.';
+
+ context.ui.addItem(
+ {
+ type: MessageType.INFO,
+ text: successMessage,
+ },
+ Date.now(),
+ );
+ }
+ } catch (error) {
+ const errorMessage = getErrorMessage(error);
+ context.ui.addItem(
+ {
+ type: MessageType.ERROR,
+ text: `Error refreshing memory: ${errorMessage}`,
+ },
+ Date.now(),
+ );
+ }
+ },
+ },
+ ],
+};
diff --git a/packages/cli/src/ui/commands/privacyCommand.test.ts b/packages/cli/src/ui/commands/privacyCommand.test.ts
new file mode 100644
index 00000000..691e5be8
--- /dev/null
+++ b/packages/cli/src/ui/commands/privacyCommand.test.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach } from 'vitest';
+import { privacyCommand } from './privacyCommand.js';
+import { type CommandContext } from './types.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+
+describe('privacyCommand', () => {
+ let mockContext: CommandContext;
+
+ beforeEach(() => {
+ mockContext = createMockCommandContext();
+ });
+
+ it('should return a dialog action to open the privacy dialog', () => {
+ // Ensure the command has an action to test.
+ if (!privacyCommand.action) {
+ throw new Error('The privacy command must have an action.');
+ }
+
+ const result = privacyCommand.action(mockContext, '');
+
+ // Assert that the action returns the correct object to trigger the privacy dialog.
+ expect(result).toEqual({
+ type: 'dialog',
+ dialog: 'privacy',
+ });
+ });
+
+ it('should have the correct name and description', () => {
+ expect(privacyCommand.name).toBe('privacy');
+ expect(privacyCommand.description).toBe('display the privacy notice');
+ });
+});
diff --git a/packages/cli/src/ui/commands/privacyCommand.ts b/packages/cli/src/ui/commands/privacyCommand.ts
new file mode 100644
index 00000000..f239158c
--- /dev/null
+++ b/packages/cli/src/ui/commands/privacyCommand.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { OpenDialogActionReturn, SlashCommand } from './types.js';
+
+export const privacyCommand: SlashCommand = {
+ name: 'privacy',
+ description: 'display the privacy notice',
+ action: (): OpenDialogActionReturn => ({
+ type: 'dialog',
+ dialog: 'privacy',
+ }),
+};
diff --git a/packages/cli/src/ui/commands/themeCommand.test.ts b/packages/cli/src/ui/commands/themeCommand.test.ts
new file mode 100644
index 00000000..2a537bcc
--- /dev/null
+++ b/packages/cli/src/ui/commands/themeCommand.test.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach } from 'vitest';
+import { themeCommand } from './themeCommand.js';
+import { type CommandContext } from './types.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+
+describe('themeCommand', () => {
+ let mockContext: CommandContext;
+
+ beforeEach(() => {
+ mockContext = createMockCommandContext();
+ });
+
+ it('should return a dialog action to open the theme dialog', () => {
+ // Ensure the command has an action to test.
+ if (!themeCommand.action) {
+ throw new Error('The theme command must have an action.');
+ }
+
+ const result = themeCommand.action(mockContext, '');
+
+ // Assert that the action returns the correct object to trigger the theme dialog.
+ expect(result).toEqual({
+ type: 'dialog',
+ dialog: 'theme',
+ });
+ });
+
+ it('should have the correct name and description', () => {
+ expect(themeCommand.name).toBe('theme');
+ expect(themeCommand.description).toBe('change the theme');
+ });
+});
diff --git a/packages/cli/src/ui/commands/themeCommand.ts b/packages/cli/src/ui/commands/themeCommand.ts
new file mode 100644
index 00000000..29e9a491
--- /dev/null
+++ b/packages/cli/src/ui/commands/themeCommand.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { OpenDialogActionReturn, SlashCommand } from './types.js';
+
+export const themeCommand: SlashCommand = {
+ name: 'theme',
+ description: 'change the theme',
+ action: (_context, _args): OpenDialogActionReturn => ({
+ type: 'dialog',
+ dialog: 'theme',
+ }),
+};
diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts
new file mode 100644
index 00000000..3a31738d
--- /dev/null
+++ b/packages/cli/src/ui/commands/types.ts
@@ -0,0 +1,98 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Config, GitService, Logger } from '@qwen/qwen-code-core';
+import { LoadedSettings } from '../../config/settings.js';
+import { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
+import { SessionStatsState } from '../contexts/SessionContext.js';
+
+// Grouped dependencies for clarity and easier mocking
+export interface CommandContext {
+ // Core services and configuration
+ services: {
+ // TODO(abhipatel12): Ensure that config is never null.
+ config: Config | null;
+ settings: LoadedSettings;
+ git: GitService | undefined;
+ logger: Logger;
+ };
+ // UI state and history management
+ ui: {
+ // TODO - As more commands are add some additions may be needed or reworked using this new context.
+ // Ex.
+ // history: HistoryItem[];
+ // pendingHistoryItems: HistoryItemWithoutId[];
+
+ /** Adds a new item to the history display. */
+ addItem: UseHistoryManagerReturn['addItem'];
+ /** Clears all history items and the console screen. */
+ clear: () => void;
+ /**
+ * Sets the transient debug message displayed in the application footer in debug mode.
+ */
+ setDebugMessage: (message: string) => void;
+ };
+ // Session-specific data
+ session: {
+ stats: SessionStatsState;
+ };
+}
+
+/**
+ * The return type for a command action that results in scheduling a tool call.
+ */
+export interface ToolActionReturn {
+ type: 'tool';
+ toolName: string;
+ toolArgs: Record;
+}
+
+/**
+ * The return type for a command action that results in a simple message
+ * being displayed to the user.
+ */
+export interface MessageActionReturn {
+ type: 'message';
+ messageType: 'info' | 'error';
+ content: string;
+}
+
+/**
+ * The return type for a command action that needs to open a dialog.
+ */
+export interface OpenDialogActionReturn {
+ type: 'dialog';
+ // TODO: Add 'theme' | 'auth' | 'editor' | 'privacy' as migration happens.
+ dialog: 'help' | 'auth' | 'theme' | 'privacy';
+}
+
+export type SlashCommandActionReturn =
+ | ToolActionReturn
+ | MessageActionReturn
+ | OpenDialogActionReturn;
+// The standardized contract for any command in the system.
+export interface SlashCommand {
+ name: string;
+ altName?: string;
+ description?: string;
+
+ // The action to run. Optional for parent commands that only group sub-commands.
+ action?: (
+ context: CommandContext,
+ args: string,
+ ) =>
+ | void
+ | SlashCommandActionReturn
+ | Promise;
+
+ // Provides argument completion (e.g., completing a tag for `/chat resume `).
+ completion?: (
+ context: CommandContext,
+ partialArg: string,
+ ) => Promise;
+
+ subCommands?: SlashCommand[];
+}
diff --git a/packages/cli/src/ui/components/AboutBox.tsx b/packages/cli/src/ui/components/AboutBox.tsx
new file mode 100644
index 00000000..71afbdd4
--- /dev/null
+++ b/packages/cli/src/ui/components/AboutBox.tsx
@@ -0,0 +1,119 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
+
+interface AboutBoxProps {
+ cliVersion: string;
+ osVersion: string;
+ sandboxEnv: string;
+ modelVersion: string;
+ selectedAuthType: string;
+ gcpProject: string;
+}
+
+export const AboutBox: React.FC = ({
+ cliVersion,
+ osVersion,
+ sandboxEnv,
+ modelVersion,
+ selectedAuthType,
+ gcpProject,
+}) => (
+
+
+
+ About Gemini CLI
+
+
+
+
+
+ CLI Version
+
+
+
+ {cliVersion}
+
+
+ {GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
+
+
+
+ Git Commit
+
+
+
+ {GIT_COMMIT_INFO}
+
+
+ )}
+
+
+
+ Model
+
+
+
+ {modelVersion}
+
+
+
+
+
+ Sandbox
+
+
+
+ {sandboxEnv}
+
+
+
+
+
+ OS
+
+
+
+ {osVersion}
+
+
+
+
+
+ Auth Method
+
+
+
+
+ {selectedAuthType.startsWith('oauth') ? 'OAuth' : selectedAuthType}
+
+
+
+ {gcpProject && (
+
+
+
+ GCP Project
+
+
+
+ {gcpProject}
+
+
+ )}
+
+);
diff --git a/packages/cli/src/ui/components/AsciiArt.ts b/packages/cli/src/ui/components/AsciiArt.ts
new file mode 100644
index 00000000..3202c29e
--- /dev/null
+++ b/packages/cli/src/ui/components/AsciiArt.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const shortAsciiLogo = `
+ โโโโโโโ โโโ โโโโโโโโโโโโโโโ โโโ
+โโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโ
+โโโ โโโโโโ โโ โโโโโโโโโ โโโโโโ โโโ
+โโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโ
+ โโโโโโโ โโโโโโโโ โโโโโโโโโโโ โโโโโ
+`;
+export const longAsciiLogo = `
+โโโ โโโโโโโ โโโ โโโโโโโโโโโโโโโ โโโ
+โโโโ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโ
+ โโโโ โโโ โโโโโโ โโ โโโโโโโโโ โโโโโโ โโโ
+ โโโโ โโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโ
+โโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโ
+โโโ โโโโโโโ โโโโโโโโ โโโโโโโโโโโ โโโโโ
+`;
diff --git a/packages/cli/src/ui/components/AuthDialog.test.tsx b/packages/cli/src/ui/components/AuthDialog.test.tsx
new file mode 100644
index 00000000..20d64656
--- /dev/null
+++ b/packages/cli/src/ui/components/AuthDialog.test.tsx
@@ -0,0 +1,325 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { AuthDialog } from './AuthDialog.js';
+import { LoadedSettings, SettingScope } from '../../config/settings.js';
+import { AuthType } from '@qwen/qwen-code-core';
+
+describe('AuthDialog', () => {
+ const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
+
+ let originalEnv: NodeJS.ProcessEnv;
+
+ beforeEach(() => {
+ originalEnv = { ...process.env };
+ process.env.GEMINI_API_KEY = '';
+ process.env.GEMINI_DEFAULT_AUTH_TYPE = '';
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ it('should show an error if the initial auth type is invalid', () => {
+ process.env.GEMINI_API_KEY = '';
+
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {},
+ path: '',
+ },
+ {
+ settings: {
+ selectedAuthType: AuthType.USE_GEMINI,
+ },
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { lastFrame } = render(
+ {}}
+ settings={settings}
+ initialErrorMessage="GEMINI_API_KEY environment variable not found"
+ />,
+ );
+
+ expect(lastFrame()).toContain(
+ 'GEMINI_API_KEY environment variable not found',
+ );
+ });
+
+ describe('GEMINI_API_KEY environment variable', () => {
+ it('should detect GEMINI_API_KEY environment variable', () => {
+ process.env.GEMINI_API_KEY = 'foobar';
+
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {
+ selectedAuthType: undefined,
+ },
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { lastFrame } = render(
+ {}} settings={settings} />,
+ );
+
+ // Since the auth dialog only shows OpenAI option now,
+ // it won't show GEMINI_API_KEY messages
+ expect(lastFrame()).toContain('OpenAI');
+ });
+
+ it('should not show the GEMINI_API_KEY message if GEMINI_DEFAULT_AUTH_TYPE is set to something else', () => {
+ process.env.GEMINI_API_KEY = 'foobar';
+ process.env.GEMINI_DEFAULT_AUTH_TYPE = AuthType.LOGIN_WITH_GOOGLE;
+
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {
+ selectedAuthType: undefined,
+ },
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { lastFrame } = render(
+ {}} settings={settings} />,
+ );
+
+ expect(lastFrame()).not.toContain(
+ 'Existing API key detected (GEMINI_API_KEY)',
+ );
+ });
+
+ it('should show the GEMINI_API_KEY message if GEMINI_DEFAULT_AUTH_TYPE is set to use api key', () => {
+ process.env.GEMINI_API_KEY = 'foobar';
+ process.env.GEMINI_DEFAULT_AUTH_TYPE = AuthType.USE_GEMINI;
+
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {
+ selectedAuthType: undefined,
+ },
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { lastFrame } = render(
+ {}} settings={settings} />,
+ );
+
+ // Since the auth dialog only shows OpenAI option now,
+ // it won't show GEMINI_API_KEY messages
+ expect(lastFrame()).toContain('OpenAI');
+ });
+ });
+
+ describe('GEMINI_DEFAULT_AUTH_TYPE environment variable', () => {
+ it('should select the auth type specified by GEMINI_DEFAULT_AUTH_TYPE', () => {
+ process.env.GEMINI_DEFAULT_AUTH_TYPE = AuthType.LOGIN_WITH_GOOGLE;
+
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {
+ selectedAuthType: undefined,
+ },
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { lastFrame } = render(
+ {}} settings={settings} />,
+ );
+
+ // Since only OpenAI is available, it should be selected by default
+ expect(lastFrame()).toContain('โ OpenAI');
+ });
+
+ it('should fall back to default if GEMINI_DEFAULT_AUTH_TYPE is not set', () => {
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {
+ selectedAuthType: undefined,
+ },
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { lastFrame } = render(
+ {}} settings={settings} />,
+ );
+
+ // Default is OpenAI (the only option)
+ expect(lastFrame()).toContain('โ OpenAI');
+ });
+
+ it('should show an error and fall back to default if GEMINI_DEFAULT_AUTH_TYPE is invalid', () => {
+ process.env.GEMINI_DEFAULT_AUTH_TYPE = 'invalid-auth-type';
+
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {
+ selectedAuthType: undefined,
+ },
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { lastFrame } = render(
+ {}} settings={settings} />,
+ );
+
+ // Since the auth dialog doesn't show GEMINI_DEFAULT_AUTH_TYPE errors anymore,
+ // it will just show the default OpenAI option
+ expect(lastFrame()).toContain('โ OpenAI');
+ });
+ });
+
+ // it('should prevent exiting when no auth method is selected and show error message', async () => {
+ // const onSelect = vi.fn();
+ // const settings: LoadedSettings = new LoadedSettings(
+ // {
+ // settings: {},
+ // path: '',
+ // },
+ // {
+ // settings: {
+ // selectedAuthType: undefined,
+ // },
+ // path: '',
+ // },
+ // {
+ // settings: {},
+ // path: '',
+ // },
+ // [],
+ // );
+
+ // const { lastFrame, stdin, unmount } = render(
+ // ,
+ // );
+ // await wait();
+
+ // // Simulate pressing escape key
+ // stdin.write('\u001b'); // ESC key
+ // await wait(100); // Increased wait time for CI environment
+
+ // // Should show error message instead of calling onSelect
+ // expect(lastFrame()).toContain(
+ // 'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
+ // );
+ // expect(onSelect).not.toHaveBeenCalled();
+ // unmount();
+ // });
+
+ it('should not exit if there is already an error message', async () => {
+ const onSelect = vi.fn();
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {},
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { lastFrame, stdin, unmount } = render(
+ ,
+ );
+ await wait();
+
+ expect(lastFrame()).toContain('Initial error');
+
+ // Simulate pressing escape key
+ stdin.write('\u001b'); // ESC key
+ await wait();
+
+ // Should not call onSelect
+ expect(onSelect).not.toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should allow exiting when auth method is already selected', async () => {
+ const onSelect = vi.fn();
+ const settings: LoadedSettings = new LoadedSettings(
+ {
+ settings: {},
+ path: '',
+ },
+ {
+ settings: {
+ selectedAuthType: AuthType.USE_GEMINI,
+ },
+ path: '',
+ },
+ {
+ settings: {},
+ path: '',
+ },
+ [],
+ );
+
+ const { stdin, unmount } = render(
+ ,
+ );
+ await wait();
+
+ // Simulate pressing escape key
+ stdin.write('\u001b'); // ESC key
+ await wait();
+
+ // Should call onSelect with undefined to exit
+ expect(onSelect).toHaveBeenCalledWith(undefined, SettingScope.User);
+ unmount();
+ });
+});
diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx
new file mode 100644
index 00000000..39bb72b0
--- /dev/null
+++ b/packages/cli/src/ui/components/AuthDialog.tsx
@@ -0,0 +1,171 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { Colors } from '../colors.js';
+import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
+import { LoadedSettings, SettingScope } from '../../config/settings.js';
+import { AuthType } from '@qwen/qwen-code-core';
+import {
+ validateAuthMethod,
+ setOpenAIApiKey,
+ setOpenAIBaseUrl,
+ setOpenAIModel,
+} from '../../config/auth.js';
+import { OpenAIKeyPrompt } from './OpenAIKeyPrompt.js';
+
+interface AuthDialogProps {
+ onSelect: (authMethod: AuthType | undefined, scope: SettingScope) => void;
+ settings: LoadedSettings;
+ initialErrorMessage?: string | null;
+}
+
+function parseDefaultAuthType(
+ defaultAuthType: string | undefined,
+): AuthType | null {
+ if (
+ defaultAuthType &&
+ Object.values(AuthType).includes(defaultAuthType as AuthType)
+ ) {
+ return defaultAuthType as AuthType;
+ }
+ return null;
+}
+
+export function AuthDialog({
+ onSelect,
+ settings,
+ initialErrorMessage,
+}: AuthDialogProps): React.JSX.Element {
+ const [errorMessage, setErrorMessage] = useState(
+ initialErrorMessage || null,
+ );
+ const [showOpenAIKeyPrompt, setShowOpenAIKeyPrompt] = useState(false);
+ const items = [{ label: 'OpenAI', value: AuthType.USE_OPENAI }];
+
+ const initialAuthIndex = items.findIndex((item) => {
+ if (settings.merged.selectedAuthType) {
+ return item.value === settings.merged.selectedAuthType;
+ }
+
+ const defaultAuthType = parseDefaultAuthType(
+ process.env.GEMINI_DEFAULT_AUTH_TYPE,
+ );
+ if (defaultAuthType) {
+ return item.value === defaultAuthType;
+ }
+
+ if (process.env.GEMINI_API_KEY) {
+ return item.value === AuthType.USE_GEMINI;
+ }
+
+ return item.value === AuthType.LOGIN_WITH_GOOGLE;
+ });
+
+ const handleAuthSelect = (authMethod: AuthType) => {
+ const error = validateAuthMethod(authMethod);
+ if (error) {
+ if (authMethod === AuthType.USE_OPENAI && !process.env.OPENAI_API_KEY) {
+ setShowOpenAIKeyPrompt(true);
+ setErrorMessage(null);
+ } else {
+ setErrorMessage(error);
+ }
+ } else {
+ setErrorMessage(null);
+ onSelect(authMethod, SettingScope.User);
+ }
+ };
+
+ const handleOpenAIKeySubmit = (
+ apiKey: string,
+ baseUrl: string,
+ model: string,
+ ) => {
+ setOpenAIApiKey(apiKey);
+ setOpenAIBaseUrl(baseUrl);
+ setOpenAIModel(model);
+ setShowOpenAIKeyPrompt(false);
+ onSelect(AuthType.USE_OPENAI, SettingScope.User);
+ };
+
+ const handleOpenAIKeyCancel = () => {
+ setShowOpenAIKeyPrompt(false);
+ setErrorMessage('OpenAI API key is required to use OpenAI authentication.');
+ };
+
+ useInput((_input, key) => {
+ // ๅฝๆพ็คบ OpenAIKeyPrompt ๆถ๏ผไธๅค็่พๅ
ฅไบไปถ
+ if (showOpenAIKeyPrompt) {
+ return;
+ }
+
+ if (key.escape) {
+ // Prevent exit if there is an error message.
+ // This means they user is not authenticated yet.
+ if (errorMessage) {
+ return;
+ }
+ if (settings.merged.selectedAuthType === undefined) {
+ // Prevent exiting if no auth method is set
+ setErrorMessage(
+ 'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
+ );
+ return;
+ }
+ onSelect(undefined, SettingScope.User);
+ }
+ });
+
+ if (showOpenAIKeyPrompt) {
+ return (
+
+ );
+ }
+
+ return (
+
+ Get started
+
+ How would you like to authenticate for this project?
+
+
+
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+ (Use Enter to Set Auth)
+
+
+ Terms of Services and Privacy Notice for Qwen Code
+
+
+
+ {'https://github.com/QwenLM/Qwen3-Coder/blob/main/README.md'}
+
+
+
+ );
+}
diff --git a/packages/cli/src/ui/components/AuthInProgress.tsx b/packages/cli/src/ui/components/AuthInProgress.tsx
new file mode 100644
index 00000000..196097f2
--- /dev/null
+++ b/packages/cli/src/ui/components/AuthInProgress.tsx
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState, useEffect } from 'react';
+import { Box, Text, useInput } from 'ink';
+import Spinner from 'ink-spinner';
+import { Colors } from '../colors.js';
+
+interface AuthInProgressProps {
+ onTimeout: () => void;
+}
+
+export function AuthInProgress({
+ onTimeout,
+}: AuthInProgressProps): React.JSX.Element {
+ const [timedOut, setTimedOut] = useState(false);
+
+ useInput((_, key) => {
+ if (key.escape) {
+ onTimeout();
+ }
+ });
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setTimedOut(true);
+ onTimeout();
+ }, 180000);
+
+ return () => clearTimeout(timer);
+ }, [onTimeout]);
+
+ return (
+
+ {timedOut ? (
+
+ Authentication timed out. Please try again.
+
+ ) : (
+
+
+ Waiting for auth... (Press ESC to cancel)
+
+
+ )}
+
+ );
+}
diff --git a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx
new file mode 100644
index 00000000..a4556fa4
--- /dev/null
+++ b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { ApprovalMode } from '@qwen/qwen-code-core';
+
+interface AutoAcceptIndicatorProps {
+ approvalMode: ApprovalMode;
+}
+
+export const AutoAcceptIndicator: React.FC = ({
+ approvalMode,
+}) => {
+ let textColor = '';
+ let textContent = '';
+ let subText = '';
+
+ switch (approvalMode) {
+ case ApprovalMode.AUTO_EDIT:
+ textColor = Colors.AccentGreen;
+ textContent = 'accepting edits';
+ subText = ' (shift + tab to toggle)';
+ break;
+ case ApprovalMode.YOLO:
+ textColor = Colors.AccentRed;
+ textContent = 'YOLO mode';
+ subText = ' (ctrl + y to toggle)';
+ break;
+ case ApprovalMode.DEFAULT:
+ default:
+ break;
+ }
+
+ return (
+
+
+ {textContent}
+ {subText && {subText}}
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/ConsoleSummaryDisplay.tsx b/packages/cli/src/ui/components/ConsoleSummaryDisplay.tsx
new file mode 100644
index 00000000..c79cc096
--- /dev/null
+++ b/packages/cli/src/ui/components/ConsoleSummaryDisplay.tsx
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+
+interface ConsoleSummaryDisplayProps {
+ errorCount: number;
+ // logCount is not currently in the plan to be displayed in summary
+}
+
+export const ConsoleSummaryDisplay: React.FC = ({
+ errorCount,
+}) => {
+ if (errorCount === 0) {
+ return null;
+ }
+
+ const errorIcon = '\u2716'; // Heavy multiplication x (โ)
+
+ return (
+
+ {errorCount > 0 && (
+
+ {errorIcon} {errorCount} error{errorCount > 1 ? 's' : ''}{' '}
+ (ctrl+o for details)
+
+ )}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx
new file mode 100644
index 00000000..36da5540
--- /dev/null
+++ b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Text } from 'ink';
+import { Colors } from '../colors.js';
+import { type MCPServerConfig } from '@qwen/qwen-code-core';
+
+interface ContextSummaryDisplayProps {
+ geminiMdFileCount: number;
+ contextFileNames: string[];
+ mcpServers?: Record;
+ showToolDescriptions?: boolean;
+}
+
+export const ContextSummaryDisplay: React.FC = ({
+ geminiMdFileCount,
+ contextFileNames,
+ mcpServers,
+ showToolDescriptions,
+}) => {
+ const mcpServerCount = Object.keys(mcpServers || {}).length;
+
+ if (geminiMdFileCount === 0 && mcpServerCount === 0) {
+ return ; // Render an empty space to reserve height
+ }
+
+ const geminiMdText = (() => {
+ if (geminiMdFileCount === 0) {
+ return '';
+ }
+ const allNamesTheSame = new Set(contextFileNames).size < 2;
+ const name = allNamesTheSame ? contextFileNames[0] : 'context';
+ return `${geminiMdFileCount} ${name} file${
+ geminiMdFileCount > 1 ? 's' : ''
+ }`;
+ })();
+
+ const mcpText =
+ mcpServerCount > 0
+ ? `${mcpServerCount} MCP server${mcpServerCount > 1 ? 's' : ''}`
+ : '';
+
+ let summaryText = 'Using ';
+ if (geminiMdText) {
+ summaryText += geminiMdText;
+ }
+ if (geminiMdText && mcpText) {
+ summaryText += ' and ';
+ }
+ if (mcpText) {
+ summaryText += mcpText;
+ // Add ctrl+t hint when MCP servers are available
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
+ if (showToolDescriptions) {
+ summaryText += ' (ctrl+t to toggle)';
+ } else {
+ summaryText += ' (ctrl+t to view)';
+ }
+ }
+ }
+
+ return {summaryText};
+};
diff --git a/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx
new file mode 100644
index 00000000..2bc5d392
--- /dev/null
+++ b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { ConsoleMessageItem } from '../types.js';
+import { MaxSizedBox } from './shared/MaxSizedBox.js';
+
+interface DetailedMessagesDisplayProps {
+ messages: ConsoleMessageItem[];
+ maxHeight: number | undefined;
+ width: number;
+ // debugMode is not needed here if App.tsx filters debug messages before passing them.
+ // If DetailedMessagesDisplay should handle filtering, add debugMode prop.
+}
+
+export const DetailedMessagesDisplay: React.FC<
+ DetailedMessagesDisplayProps
+> = ({ messages, maxHeight, width }) => {
+ if (messages.length === 0) {
+ return null; // Don't render anything if there are no messages
+ }
+
+ const borderAndPadding = 4;
+ return (
+
+
+
+ Debug Console (ctrl+o to close)
+
+
+
+ {messages.map((msg, index) => {
+ let textColor = Colors.Foreground;
+ let icon = '\u2139'; // Information source (โน)
+
+ switch (msg.type) {
+ case 'warn':
+ textColor = Colors.AccentYellow;
+ icon = '\u26A0'; // Warning sign (โ )
+ break;
+ case 'error':
+ textColor = Colors.AccentRed;
+ icon = '\u2716'; // Heavy multiplication x (โ)
+ break;
+ case 'debug':
+ textColor = Colors.Gray; // Or Colors.Gray
+ icon = '\u1F50D'; // Left-pointing magnifying glass (????)
+ break;
+ case 'log':
+ default:
+ // Default textColor and icon are already set
+ break;
+ }
+
+ return (
+
+ {icon}
+
+ {msg.content}
+ {msg.count && msg.count > 1 && (
+ (x{msg.count})
+ )}
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.tsx
new file mode 100644
index 00000000..179516a8
--- /dev/null
+++ b/packages/cli/src/ui/components/EditorSettingsDialog.tsx
@@ -0,0 +1,168 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { Colors } from '../colors.js';
+import {
+ EDITOR_DISPLAY_NAMES,
+ editorSettingsManager,
+ type EditorDisplay,
+} from '../editors/editorSettingsManager.js';
+import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
+import { LoadedSettings, SettingScope } from '../../config/settings.js';
+import { EditorType, isEditorAvailable } from '@qwen/qwen-code-core';
+
+interface EditorDialogProps {
+ onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void;
+ settings: LoadedSettings;
+ onExit: () => void;
+}
+
+export function EditorSettingsDialog({
+ onSelect,
+ settings,
+ onExit,
+}: EditorDialogProps): React.JSX.Element {
+ const [selectedScope, setSelectedScope] = useState(
+ SettingScope.User,
+ );
+ const [focusedSection, setFocusedSection] = useState<'editor' | 'scope'>(
+ 'editor',
+ );
+ useInput((_, key) => {
+ if (key.tab) {
+ setFocusedSection((prev) => (prev === 'editor' ? 'scope' : 'editor'));
+ }
+ if (key.escape) {
+ onExit();
+ }
+ });
+
+ const editorItems: EditorDisplay[] =
+ editorSettingsManager.getAvailableEditorDisplays();
+
+ const currentPreference =
+ settings.forScope(selectedScope).settings.preferredEditor;
+ let editorIndex = currentPreference
+ ? editorItems.findIndex(
+ (item: EditorDisplay) => item.type === currentPreference,
+ )
+ : 0;
+ if (editorIndex === -1) {
+ console.error(`Editor is not supported: ${currentPreference}`);
+ editorIndex = 0;
+ }
+
+ const scopeItems = [
+ { label: 'User Settings', value: SettingScope.User },
+ { label: 'Workspace Settings', value: SettingScope.Workspace },
+ ];
+
+ const handleEditorSelect = (editorType: EditorType | 'not_set') => {
+ if (editorType === 'not_set') {
+ onSelect(undefined, selectedScope);
+ return;
+ }
+ onSelect(editorType, selectedScope);
+ };
+
+ const handleScopeSelect = (scope: SettingScope) => {
+ setSelectedScope(scope);
+ setFocusedSection('editor');
+ };
+
+ let otherScopeModifiedMessage = '';
+ const otherScope =
+ selectedScope === SettingScope.User
+ ? SettingScope.Workspace
+ : SettingScope.User;
+ if (settings.forScope(otherScope).settings.preferredEditor !== undefined) {
+ otherScopeModifiedMessage =
+ settings.forScope(selectedScope).settings.preferredEditor !== undefined
+ ? `(Also modified in ${otherScope})`
+ : `(Modified in ${otherScope})`;
+ }
+
+ let mergedEditorName = 'None';
+ if (
+ settings.merged.preferredEditor &&
+ isEditorAvailable(settings.merged.preferredEditor)
+ ) {
+ mergedEditorName =
+ EDITOR_DISPLAY_NAMES[settings.merged.preferredEditor as EditorType];
+ }
+
+ return (
+
+
+
+ {focusedSection === 'editor' ? '> ' : ' '}Select Editor{' '}
+ {otherScopeModifiedMessage}
+
+ ({
+ label: item.name,
+ value: item.type,
+ disabled: item.disabled,
+ }))}
+ initialIndex={editorIndex}
+ onSelect={handleEditorSelect}
+ isFocused={focusedSection === 'editor'}
+ key={selectedScope}
+ />
+
+
+
+ {focusedSection === 'scope' ? '> ' : ' '}Apply To
+
+
+
+
+
+
+ (Use Enter to select, Tab to change focus)
+
+
+
+
+
+ Editor Preference
+
+
+ These editors are currently supported. Please note that some editors
+ cannot be used in sandbox mode.
+
+
+ Your preferred editor is:{' '}
+
+ {mergedEditorName}
+
+ .
+
+
+
+
+ );
+}
diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx
new file mode 100644
index 00000000..0a9e741b
--- /dev/null
+++ b/packages/cli/src/ui/components/Footer.tsx
@@ -0,0 +1,121 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { shortenPath, tildeifyPath, tokenLimit } from '@qwen/qwen-code-core';
+import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
+import process from 'node:process';
+import Gradient from 'ink-gradient';
+import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
+
+interface FooterProps {
+ model: string;
+ targetDir: string;
+ branchName?: string;
+ debugMode: boolean;
+ debugMessage: string;
+ corgiMode: boolean;
+ errorCount: number;
+ showErrorDetails: boolean;
+ showMemoryUsage?: boolean;
+ promptTokenCount: number;
+ nightly: boolean;
+}
+
+export const Footer: React.FC = ({
+ model,
+ targetDir,
+ branchName,
+ debugMode,
+ debugMessage,
+ corgiMode,
+ errorCount,
+ showErrorDetails,
+ showMemoryUsage,
+ promptTokenCount,
+ nightly,
+}) => {
+ const limit = tokenLimit(model);
+ const percentage = promptTokenCount / limit;
+
+ return (
+
+
+ {nightly ? (
+
+
+ {shortenPath(tildeifyPath(targetDir), 70)}
+ {branchName && ({branchName}*)}
+
+
+ ) : (
+
+ {shortenPath(tildeifyPath(targetDir), 70)}
+ {branchName && ({branchName}*)}
+
+ )}
+ {debugMode && (
+
+ {' ' + (debugMessage || '--debug')}
+
+ )}
+
+
+ {/* Middle Section: Centered Sandbox Info */}
+
+ {process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec' ? (
+
+ {process.env.SANDBOX.replace(/^gemini-(?:cli-)?/, '')}
+
+ ) : process.env.SANDBOX === 'sandbox-exec' ? (
+
+ MacOS Seatbelt{' '}
+ ({process.env.SEATBELT_PROFILE})
+
+ ) : (
+
+ no sandbox (see /docs)
+
+ )}
+
+
+ {/* Right Section: Gemini Label and Console Summary */}
+
+
+ {' '}
+ {model}{' '}
+
+ ({((1 - percentage) * 100).toFixed(0)}% context left)
+
+
+ {corgiMode && (
+
+ |
+ โผ
+ (ยด
+ แดฅ
+ `)
+ โผ
+
+ )}
+ {!showErrorDetails && errorCount > 0 && (
+
+ |
+
+
+ )}
+ {showMemoryUsage && }
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx
new file mode 100644
index 00000000..97e10cb3
--- /dev/null
+++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx
@@ -0,0 +1,34 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Text } from 'ink';
+import Spinner from 'ink-spinner';
+import type { SpinnerName } from 'cli-spinners';
+import { useStreamingContext } from '../contexts/StreamingContext.js';
+import { StreamingState } from '../types.js';
+
+interface GeminiRespondingSpinnerProps {
+ /**
+ * Optional string to display when not in Responding state.
+ * If not provided and not Responding, renders null.
+ */
+ nonRespondingDisplay?: string;
+ spinnerType?: SpinnerName;
+}
+
+export const GeminiRespondingSpinner: React.FC<
+ GeminiRespondingSpinnerProps
+> = ({ nonRespondingDisplay, spinnerType = 'dots' }) => {
+ const streamingState = useStreamingContext();
+
+ if (streamingState === StreamingState.Responding) {
+ return ;
+ } else if (nonRespondingDisplay) {
+ return {nonRespondingDisplay};
+ }
+ return null;
+};
diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx
new file mode 100644
index 00000000..b99382e0
--- /dev/null
+++ b/packages/cli/src/ui/components/Header.tsx
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import Gradient from 'ink-gradient';
+import { Colors } from '../colors.js';
+import { shortAsciiLogo, longAsciiLogo } from './AsciiArt.js';
+import { getAsciiArtWidth } from '../utils/textUtils.js';
+
+interface HeaderProps {
+ customAsciiArt?: string; // For user-defined ASCII art
+ terminalWidth: number; // For responsive logo
+ version: string;
+ nightly: boolean;
+}
+
+export const Header: React.FC = ({
+ customAsciiArt,
+ terminalWidth,
+ version,
+ nightly,
+}) => {
+ let displayTitle;
+ const widthOfLongLogo = getAsciiArtWidth(longAsciiLogo);
+
+ if (customAsciiArt) {
+ displayTitle = customAsciiArt;
+ } else {
+ displayTitle =
+ terminalWidth >= widthOfLongLogo ? longAsciiLogo : shortAsciiLogo;
+ }
+
+ const artWidth = getAsciiArtWidth(displayTitle);
+
+ return (
+
+ {Colors.GradientColors ? (
+
+ {displayTitle}
+
+ ) : (
+ {displayTitle}
+ )}
+ {nightly && (
+
+
+ v{version}
+
+
+ )}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/Help.tsx b/packages/cli/src/ui/components/Help.tsx
new file mode 100644
index 00000000..c51867af
--- /dev/null
+++ b/packages/cli/src/ui/components/Help.tsx
@@ -0,0 +1,155 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { SlashCommand } from '../commands/types.js';
+
+interface Help {
+ commands: SlashCommand[];
+}
+
+export const Help: React.FC = ({ commands }) => (
+
+ {/* Basics */}
+
+ Basics:
+
+
+
+ Add context
+
+ : Use{' '}
+
+ @
+ {' '}
+ to specify files for context (e.g.,{' '}
+
+ @src/myFile.ts
+
+ ) to target specific files or folders.
+
+
+
+ Shell mode
+
+ : Execute shell commands via{' '}
+
+ !
+ {' '}
+ (e.g.,{' '}
+
+ !npm run start
+
+ ) or use natural language (e.g.{' '}
+
+ start server
+
+ ).
+
+
+
+
+ {/* Commands */}
+
+ Commands:
+
+ {commands
+ .filter((command) => command.description)
+ .map((command: SlashCommand) => (
+
+
+
+ {' '}
+ /{command.name}
+
+ {command.description && ' - ' + command.description}
+
+ {command.subCommands &&
+ command.subCommands.map((subCommand) => (
+
+
+ {' '}
+ {subCommand.name}
+
+ {subCommand.description && ' - ' + subCommand.description}
+
+ ))}
+
+ ))}
+
+
+ {' '}
+ !{' '}
+
+ - shell command
+
+
+
+
+ {/* Shortcuts */}
+
+ Keyboard Shortcuts:
+
+
+
+ Enter
+ {' '}
+ - Send message
+
+
+
+ {process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'}
+ {' '}
+ {process.platform === 'linux'
+ ? '- New line (Alt+Enter works for certain linux distros)'
+ : '- New line'}
+
+
+
+ Up/Down
+ {' '}
+ - Cycle through your prompt history
+
+
+
+ Alt+Left/Right
+ {' '}
+ - Jump through words in the input
+
+
+
+ Shift+Tab
+ {' '}
+ - Toggle auto-accepting edits
+
+
+
+ Ctrl+Y
+ {' '}
+ - Toggle YOLO mode
+
+
+
+ Esc
+ {' '}
+ - Cancel operation
+
+
+
+ Ctrl+C
+ {' '}
+ - Quit application
+
+
+);
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
new file mode 100644
index 00000000..b40b20bc
--- /dev/null
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
@@ -0,0 +1,112 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { HistoryItemDisplay } from './HistoryItemDisplay.js';
+import { HistoryItem, MessageType } from '../types.js';
+import { SessionStatsProvider } from '../contexts/SessionContext.js';
+
+// Mock child components
+vi.mock('./messages/ToolGroupMessage.js', () => ({
+ ToolGroupMessage: () => ,
+}));
+
+describe('', () => {
+ const baseItem = {
+ id: 1,
+ timestamp: 12345,
+ isPending: false,
+ terminalWidth: 80,
+ };
+
+ it('renders UserMessage for "user" type', () => {
+ const item: HistoryItem = {
+ ...baseItem,
+ type: MessageType.USER,
+ text: 'Hello',
+ };
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toContain('Hello');
+ });
+
+ it('renders StatsDisplay for "stats" type', () => {
+ const item: HistoryItem = {
+ ...baseItem,
+ type: MessageType.STATS,
+ duration: '1s',
+ };
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ expect(lastFrame()).toContain('Stats');
+ });
+
+ it('renders AboutBox for "about" type', () => {
+ const item: HistoryItem = {
+ ...baseItem,
+ type: MessageType.ABOUT,
+ cliVersion: '1.0.0',
+ osVersion: 'test-os',
+ sandboxEnv: 'test-env',
+ modelVersion: 'test-model',
+ selectedAuthType: 'test-auth',
+ gcpProject: 'test-project',
+ };
+ const { lastFrame } = render(
+ ,
+ );
+ expect(lastFrame()).toContain('About Gemini CLI');
+ });
+
+ it('renders ModelStatsDisplay for "model_stats" type', () => {
+ const item: HistoryItem = {
+ ...baseItem,
+ type: 'model_stats',
+ };
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ expect(lastFrame()).toContain(
+ 'No API calls have been made in this session.',
+ );
+ });
+
+ it('renders ToolStatsDisplay for "tool_stats" type', () => {
+ const item: HistoryItem = {
+ ...baseItem,
+ type: 'tool_stats',
+ };
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ expect(lastFrame()).toContain(
+ 'No tool calls have been made in this session.',
+ );
+ });
+
+ it('renders SessionSummaryDisplay for "quit" type', () => {
+ const item: HistoryItem = {
+ ...baseItem,
+ type: 'quit',
+ duration: '1s',
+ };
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ expect(lastFrame()).toContain('Agent powering down. Goodbye!');
+ });
+});
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
new file mode 100644
index 00000000..295a85d9
--- /dev/null
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -0,0 +1,92 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import type { HistoryItem } from '../types.js';
+import { UserMessage } from './messages/UserMessage.js';
+import { UserShellMessage } from './messages/UserShellMessage.js';
+import { GeminiMessage } from './messages/GeminiMessage.js';
+import { InfoMessage } from './messages/InfoMessage.js';
+import { ErrorMessage } from './messages/ErrorMessage.js';
+import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
+import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
+import { CompressionMessage } from './messages/CompressionMessage.js';
+import { Box } from 'ink';
+import { AboutBox } from './AboutBox.js';
+import { StatsDisplay } from './StatsDisplay.js';
+import { ModelStatsDisplay } from './ModelStatsDisplay.js';
+import { ToolStatsDisplay } from './ToolStatsDisplay.js';
+import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
+import { Config } from '@qwen/qwen-code-core';
+
+interface HistoryItemDisplayProps {
+ item: HistoryItem;
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+ isPending: boolean;
+ config?: Config;
+ isFocused?: boolean;
+}
+
+export const HistoryItemDisplay: React.FC = ({
+ item,
+ availableTerminalHeight,
+ terminalWidth,
+ isPending,
+ config,
+ isFocused = true,
+}) => (
+
+ {/* Render standard message types */}
+ {item.type === 'user' && }
+ {item.type === 'user_shell' && }
+ {item.type === 'gemini' && (
+
+ )}
+ {item.type === 'gemini_content' && (
+
+ )}
+ {item.type === 'info' && }
+ {item.type === 'error' && }
+ {item.type === 'about' && (
+
+ )}
+ {item.type === 'stats' && }
+ {item.type === 'model_stats' && }
+ {item.type === 'tool_stats' && }
+ {item.type === 'quit' && }
+ {item.type === 'tool_group' && (
+
+ )}
+ {item.type === 'compression' && (
+
+ )}
+
+);
diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
new file mode 100644
index 00000000..814f2cd1
--- /dev/null
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -0,0 +1,546 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { InputPrompt, InputPromptProps } from './InputPrompt.js';
+import type { TextBuffer } from './shared/text-buffer.js';
+import { Config } from '@qwen/qwen-code-core';
+import { CommandContext, SlashCommand } from '../commands/types.js';
+import { vi } from 'vitest';
+import { useShellHistory } from '../hooks/useShellHistory.js';
+import { useCompletion } from '../hooks/useCompletion.js';
+import { useInputHistory } from '../hooks/useInputHistory.js';
+import * as clipboardUtils from '../utils/clipboardUtils.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+
+vi.mock('../hooks/useShellHistory.js');
+vi.mock('../hooks/useCompletion.js');
+vi.mock('../hooks/useInputHistory.js');
+vi.mock('../utils/clipboardUtils.js');
+
+type MockedUseShellHistory = ReturnType;
+type MockedUseCompletion = ReturnType;
+type MockedUseInputHistory = ReturnType;
+
+const mockSlashCommands: SlashCommand[] = [
+ { name: 'clear', description: 'Clear screen', action: vi.fn() },
+ {
+ name: 'memory',
+ description: 'Manage memory',
+ subCommands: [
+ { name: 'show', description: 'Show memory', action: vi.fn() },
+ { name: 'add', description: 'Add to memory', action: vi.fn() },
+ { name: 'refresh', description: 'Refresh memory', action: vi.fn() },
+ ],
+ },
+ {
+ name: 'chat',
+ description: 'Manage chats',
+ subCommands: [
+ {
+ name: 'resume',
+ description: 'Resume a chat',
+ action: vi.fn(),
+ completion: async () => ['fix-foo', 'fix-bar'],
+ },
+ ],
+ },
+];
+
+describe('InputPrompt', () => {
+ let props: InputPromptProps;
+ let mockShellHistory: MockedUseShellHistory;
+ let mockCompletion: MockedUseCompletion;
+ let mockInputHistory: MockedUseInputHistory;
+ let mockBuffer: TextBuffer;
+ let mockCommandContext: CommandContext;
+
+ const mockedUseShellHistory = vi.mocked(useShellHistory);
+ const mockedUseCompletion = vi.mocked(useCompletion);
+ const mockedUseInputHistory = vi.mocked(useInputHistory);
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+
+ mockCommandContext = createMockCommandContext();
+
+ mockBuffer = {
+ text: '',
+ cursor: [0, 0],
+ lines: [''],
+ setText: vi.fn((newText: string) => {
+ mockBuffer.text = newText;
+ mockBuffer.lines = [newText];
+ mockBuffer.cursor = [0, newText.length];
+ mockBuffer.viewportVisualLines = [newText];
+ mockBuffer.allVisualLines = [newText];
+ }),
+ replaceRangeByOffset: vi.fn(),
+ viewportVisualLines: [''],
+ allVisualLines: [''],
+ visualCursor: [0, 0],
+ visualScrollRow: 0,
+ handleInput: vi.fn(),
+ move: vi.fn(),
+ moveToOffset: vi.fn(),
+ killLineRight: vi.fn(),
+ killLineLeft: vi.fn(),
+ openInExternalEditor: vi.fn(),
+ newline: vi.fn(),
+ backspace: vi.fn(),
+ } as unknown as TextBuffer;
+
+ mockShellHistory = {
+ addCommandToHistory: vi.fn(),
+ getPreviousCommand: vi.fn().mockReturnValue(null),
+ getNextCommand: vi.fn().mockReturnValue(null),
+ resetHistoryPosition: vi.fn(),
+ };
+ mockedUseShellHistory.mockReturnValue(mockShellHistory);
+
+ mockCompletion = {
+ suggestions: [],
+ activeSuggestionIndex: -1,
+ isLoadingSuggestions: false,
+ showSuggestions: false,
+ visibleStartIndex: 0,
+ navigateUp: vi.fn(),
+ navigateDown: vi.fn(),
+ resetCompletionState: vi.fn(),
+ setActiveSuggestionIndex: vi.fn(),
+ setShowSuggestions: vi.fn(),
+ };
+ mockedUseCompletion.mockReturnValue(mockCompletion);
+
+ mockInputHistory = {
+ navigateUp: vi.fn(),
+ navigateDown: vi.fn(),
+ handleSubmit: vi.fn(),
+ };
+ mockedUseInputHistory.mockReturnValue(mockInputHistory);
+
+ props = {
+ buffer: mockBuffer,
+ onSubmit: vi.fn(),
+ userMessages: [],
+ onClearScreen: vi.fn(),
+ config: {
+ getProjectRoot: () => '/test/project',
+ getTargetDir: () => '/test/project/src',
+ } as unknown as Config,
+ slashCommands: [],
+ commandContext: mockCommandContext,
+ shellModeActive: false,
+ setShellModeActive: vi.fn(),
+ inputWidth: 80,
+ suggestionsWidth: 80,
+ focus: true,
+ };
+
+ props.slashCommands = mockSlashCommands;
+ });
+
+ const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
+
+ it('should call shellHistory.getPreviousCommand on up arrow in shell mode', async () => {
+ props.shellModeActive = true;
+ const { stdin, unmount } = render();
+ await wait(100); // Increased wait time for CI environment
+
+ stdin.write('\u001B[A');
+ await wait(100); // Increased wait time to ensure input is processed
+
+ expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should call shellHistory.getNextCommand on down arrow in shell mode', async () => {
+ props.shellModeActive = true;
+ const { stdin, unmount } = render();
+ await wait(100); // Increased wait time for CI environment
+
+ stdin.write('\u001B[B');
+ await wait(100); // Increased wait time to ensure input is processed
+
+ expect(mockShellHistory.getNextCommand).toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should set the buffer text when a shell history command is retrieved', async () => {
+ props.shellModeActive = true;
+ vi.mocked(mockShellHistory.getPreviousCommand).mockReturnValue(
+ 'previous command',
+ );
+ const { stdin, unmount } = render();
+ await wait(100); // Increased wait time for CI environment
+
+ stdin.write('\u001B[A');
+ await wait(100); // Increased wait time to ensure input is processed
+
+ expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
+ expect(props.buffer.setText).toHaveBeenCalledWith('previous command');
+ unmount();
+ });
+
+ it('should call shellHistory.addCommandToHistory on submit in shell mode', async () => {
+ props.shellModeActive = true;
+ props.buffer.setText('ls -l');
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\r');
+ await wait();
+
+ expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith('ls -l');
+ expect(props.onSubmit).toHaveBeenCalledWith('ls -l');
+ unmount();
+ });
+
+ it('should NOT call shell history methods when not in shell mode', async () => {
+ props.buffer.setText('some text');
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\u001B[A'); // Up arrow
+ await wait();
+ stdin.write('\u001B[B'); // Down arrow
+ await wait();
+ stdin.write('\r'); // Enter
+ await wait();
+
+ expect(mockShellHistory.getPreviousCommand).not.toHaveBeenCalled();
+ expect(mockShellHistory.getNextCommand).not.toHaveBeenCalled();
+ expect(mockShellHistory.addCommandToHistory).not.toHaveBeenCalled();
+
+ expect(mockInputHistory.navigateUp).toHaveBeenCalled();
+ expect(mockInputHistory.navigateDown).toHaveBeenCalled();
+ expect(props.onSubmit).toHaveBeenCalledWith('some text');
+ unmount();
+ });
+
+ describe('clipboard image paste', () => {
+ beforeEach(() => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
+ vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(null);
+ vi.mocked(clipboardUtils.cleanupOldClipboardImages).mockResolvedValue(
+ undefined,
+ );
+ });
+
+ it('should handle Ctrl+V when clipboard has an image', async () => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
+ vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(
+ '/test/.gemini-clipboard/clipboard-123.png',
+ );
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ // Send Ctrl+V
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
+ expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith(
+ props.config.getTargetDir(),
+ );
+ expect(clipboardUtils.cleanupOldClipboardImages).toHaveBeenCalledWith(
+ props.config.getTargetDir(),
+ );
+ expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should not insert anything when clipboard has no image', async () => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
+ expect(clipboardUtils.saveClipboardImage).not.toHaveBeenCalled();
+ expect(mockBuffer.setText).not.toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should handle image save failure gracefully', async () => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
+ vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(null);
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ expect(clipboardUtils.saveClipboardImage).toHaveBeenCalled();
+ expect(mockBuffer.setText).not.toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should insert image path at cursor position with proper spacing', async () => {
+ vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
+ vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(
+ '/test/.gemini-clipboard/clipboard-456.png',
+ );
+
+ // Set initial text and cursor position
+ mockBuffer.text = 'Hello world';
+ mockBuffer.cursor = [0, 5]; // Cursor after "Hello"
+ mockBuffer.lines = ['Hello world'];
+ mockBuffer.replaceRangeByOffset = vi.fn();
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ // Should insert at cursor position with spaces
+ expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
+
+ // Get the actual call to see what path was used
+ const actualCall = vi.mocked(mockBuffer.replaceRangeByOffset).mock
+ .calls[0];
+ expect(actualCall[0]).toBe(5); // start offset
+ expect(actualCall[1]).toBe(5); // end offset
+ expect(actualCall[2]).toMatch(
+ /@.*\.gemini-clipboard\/clipboard-456\.png/,
+ ); // flexible path match
+ unmount();
+ });
+
+ it('should handle errors during clipboard operations', async () => {
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+ vi.mocked(clipboardUtils.clipboardHasImage).mockRejectedValue(
+ new Error('Clipboard error'),
+ );
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\x16'); // Ctrl+V
+ await wait();
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Error handling clipboard image:',
+ expect.any(Error),
+ );
+ expect(mockBuffer.setText).not.toHaveBeenCalled();
+
+ consoleErrorSpy.mockRestore();
+ unmount();
+ });
+ });
+
+ it('should complete a partial parent command and add a space', async () => {
+ // SCENARIO: /mem -> Tab
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: true,
+ suggestions: [{ label: 'memory', value: 'memory', description: '...' }],
+ activeSuggestionIndex: 0,
+ });
+ props.buffer.setText('/mem');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\t'); // Press Tab
+ await wait();
+
+ expect(props.buffer.setText).toHaveBeenCalledWith('/memory ');
+ unmount();
+ });
+
+ it('should append a sub-command when the parent command is already complete with a space', async () => {
+ // SCENARIO: /memory -> Tab (to accept 'add')
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: true,
+ suggestions: [
+ { label: 'show', value: 'show' },
+ { label: 'add', value: 'add' },
+ ],
+ activeSuggestionIndex: 1, // 'add' is highlighted
+ });
+ props.buffer.setText('/memory ');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\t'); // Press Tab
+ await wait();
+
+ expect(props.buffer.setText).toHaveBeenCalledWith('/memory add ');
+ unmount();
+ });
+
+ it('should handle the "backspace" edge case correctly', async () => {
+ // SCENARIO: /memory -> Backspace -> /memory -> Tab (to accept 'show')
+ // This is the critical bug we fixed.
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: true,
+ suggestions: [
+ { label: 'show', value: 'show' },
+ { label: 'add', value: 'add' },
+ ],
+ activeSuggestionIndex: 0, // 'show' is highlighted
+ });
+ // The user has backspaced, so the query is now just '/memory'
+ props.buffer.setText('/memory');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\t'); // Press Tab
+ await wait();
+
+ // It should NOT become '/show '. It should correctly become '/memory show '.
+ expect(props.buffer.setText).toHaveBeenCalledWith('/memory show ');
+ unmount();
+ });
+
+ it('should complete a partial argument for a command', async () => {
+ // SCENARIO: /chat resume fi- -> Tab
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: true,
+ suggestions: [{ label: 'fix-foo', value: 'fix-foo' }],
+ activeSuggestionIndex: 0,
+ });
+ props.buffer.setText('/chat resume fi-');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\t'); // Press Tab
+ await wait();
+
+ expect(props.buffer.setText).toHaveBeenCalledWith('/chat resume fix-foo ');
+ unmount();
+ });
+
+ it('should autocomplete on Enter when suggestions are active, without submitting', async () => {
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: true,
+ suggestions: [{ label: 'memory', value: 'memory' }],
+ activeSuggestionIndex: 0,
+ });
+ props.buffer.setText('/mem');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\r');
+ await wait();
+
+ // The app should autocomplete the text, NOT submit.
+ expect(props.buffer.setText).toHaveBeenCalledWith('/memory ');
+
+ expect(props.onSubmit).not.toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should complete a command based on its altName', async () => {
+ // Add a command with an altName to our mock for this test
+ props.slashCommands.push({
+ name: 'help',
+ altName: '?',
+ description: '...',
+ });
+
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: true,
+ suggestions: [{ label: 'help', value: 'help' }],
+ activeSuggestionIndex: 0,
+ });
+ props.buffer.setText('/?');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\t'); // Press Tab
+ await wait();
+
+ expect(props.buffer.setText).toHaveBeenCalledWith('/help ');
+ unmount();
+ });
+
+ it('should not submit on Enter when the buffer is empty or only contains whitespace', async () => {
+ props.buffer.setText(' '); // Set buffer to whitespace
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\r'); // Press Enter
+ await wait();
+
+ expect(props.onSubmit).not.toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should submit directly on Enter when a complete leaf command is typed', async () => {
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: false,
+ });
+ props.buffer.setText('/clear');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\r');
+ await wait();
+
+ expect(props.onSubmit).toHaveBeenCalledWith('/clear');
+ expect(props.buffer.setText).not.toHaveBeenCalledWith('/clear ');
+ unmount();
+ });
+
+ it('should autocomplete an @-path on Enter without submitting', async () => {
+ mockedUseCompletion.mockReturnValue({
+ ...mockCompletion,
+ showSuggestions: true,
+ suggestions: [{ label: 'index.ts', value: 'index.ts' }],
+ activeSuggestionIndex: 0,
+ });
+ props.buffer.setText('@src/components/');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\r');
+ await wait();
+
+ expect(props.buffer.replaceRangeByOffset).toHaveBeenCalled();
+ expect(props.onSubmit).not.toHaveBeenCalled();
+ unmount();
+ });
+
+ it('should add a newline on enter when the line ends with a backslash', async () => {
+ props.buffer.setText('first line\\');
+
+ const { stdin, unmount } = render();
+ await wait();
+
+ stdin.write('\r');
+ await wait();
+
+ expect(props.onSubmit).not.toHaveBeenCalled();
+ expect(props.buffer.backspace).toHaveBeenCalled();
+ expect(props.buffer.newline).toHaveBeenCalled();
+ unmount();
+ });
+});
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
new file mode 100644
index 00000000..1bc70fc6
--- /dev/null
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -0,0 +1,486 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useCallback, useEffect, useState } from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { SuggestionsDisplay } from './SuggestionsDisplay.js';
+import { useInputHistory } from '../hooks/useInputHistory.js';
+import { TextBuffer } from './shared/text-buffer.js';
+import { cpSlice, cpLen } from '../utils/textUtils.js';
+import chalk from 'chalk';
+import stringWidth from 'string-width';
+import { useShellHistory } from '../hooks/useShellHistory.js';
+import { useCompletion } from '../hooks/useCompletion.js';
+import { useKeypress, Key } from '../hooks/useKeypress.js';
+import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
+import { CommandContext, SlashCommand } from '../commands/types.js';
+import { Config } from '@qwen/qwen-code-core';
+import {
+ clipboardHasImage,
+ saveClipboardImage,
+ cleanupOldClipboardImages,
+} from '../utils/clipboardUtils.js';
+import * as path from 'path';
+
+export interface InputPromptProps {
+ buffer: TextBuffer;
+ onSubmit: (value: string) => void;
+ userMessages: readonly string[];
+ onClearScreen: () => void;
+ config: Config;
+ slashCommands: SlashCommand[];
+ commandContext: CommandContext;
+ placeholder?: string;
+ focus?: boolean;
+ inputWidth: number;
+ suggestionsWidth: number;
+ shellModeActive: boolean;
+ setShellModeActive: (value: boolean) => void;
+}
+
+export const InputPrompt: React.FC = ({
+ buffer,
+ onSubmit,
+ userMessages,
+ onClearScreen,
+ config,
+ slashCommands,
+ commandContext,
+ placeholder = ' Type your message or @path/to/file',
+ focus = true,
+ inputWidth,
+ suggestionsWidth,
+ shellModeActive,
+ setShellModeActive,
+}) => {
+ const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
+ const completion = useCompletion(
+ buffer.text,
+ config.getTargetDir(),
+ isAtCommand(buffer.text) || isSlashCommand(buffer.text),
+ slashCommands,
+ commandContext,
+ config,
+ );
+
+ const resetCompletionState = completion.resetCompletionState;
+ const shellHistory = useShellHistory(config.getProjectRoot());
+
+ const handleSubmitAndClear = useCallback(
+ (submittedValue: string) => {
+ if (shellModeActive) {
+ shellHistory.addCommandToHistory(submittedValue);
+ }
+ // Clear the buffer *before* calling onSubmit to prevent potential re-submission
+ // if onSubmit triggers a re-render while the buffer still holds the old value.
+ buffer.setText('');
+ onSubmit(submittedValue);
+ resetCompletionState();
+ },
+ [onSubmit, buffer, resetCompletionState, shellModeActive, shellHistory],
+ );
+
+ const customSetTextAndResetCompletionSignal = useCallback(
+ (newText: string) => {
+ buffer.setText(newText);
+ setJustNavigatedHistory(true);
+ },
+ [buffer, setJustNavigatedHistory],
+ );
+
+ const inputHistory = useInputHistory({
+ userMessages,
+ onSubmit: handleSubmitAndClear,
+ isActive: !completion.showSuggestions && !shellModeActive,
+ currentQuery: buffer.text,
+ onChange: customSetTextAndResetCompletionSignal,
+ });
+
+ // Effect to reset completion if history navigation just occurred and set the text
+ useEffect(() => {
+ if (justNavigatedHistory) {
+ resetCompletionState();
+ setJustNavigatedHistory(false);
+ }
+ }, [
+ justNavigatedHistory,
+ buffer.text,
+ resetCompletionState,
+ setJustNavigatedHistory,
+ ]);
+
+ const completionSuggestions = completion.suggestions;
+ const handleAutocomplete = useCallback(
+ (indexToUse: number) => {
+ if (indexToUse < 0 || indexToUse >= completionSuggestions.length) {
+ return;
+ }
+ const query = buffer.text;
+ const suggestion = completionSuggestions[indexToUse].value;
+
+ if (query.trimStart().startsWith('/')) {
+ const hasTrailingSpace = query.endsWith(' ');
+ const parts = query
+ .trimStart()
+ .substring(1)
+ .split(/\s+/)
+ .filter(Boolean);
+
+ let isParentPath = false;
+ // If there's no trailing space, we need to check if the current query
+ // is already a complete path to a parent command.
+ if (!hasTrailingSpace) {
+ let currentLevel: SlashCommand[] | undefined = slashCommands;
+ for (let i = 0; i < parts.length; i++) {
+ const part = parts[i];
+ const found: SlashCommand | undefined = currentLevel?.find(
+ (cmd) => cmd.name === part || cmd.altName === part,
+ );
+
+ if (found) {
+ if (i === parts.length - 1 && found.subCommands) {
+ isParentPath = true;
+ }
+ currentLevel = found.subCommands;
+ } else {
+ // Path is invalid, so it can't be a parent path.
+ currentLevel = undefined;
+ break;
+ }
+ }
+ }
+
+ // Determine the base path of the command.
+ // - If there's a trailing space, the whole command is the base.
+ // - If it's a known parent path, the whole command is the base.
+ // - Otherwise, the base is everything EXCEPT the last partial part.
+ const basePath =
+ hasTrailingSpace || isParentPath ? parts : parts.slice(0, -1);
+ const newValue = `/${[...basePath, suggestion].join(' ')} `;
+
+ buffer.setText(newValue);
+ } else {
+ const atIndex = query.lastIndexOf('@');
+ if (atIndex === -1) return;
+ const pathPart = query.substring(atIndex + 1);
+ const lastSlashIndexInPath = pathPart.lastIndexOf('/');
+ let autoCompleteStartIndex = atIndex + 1;
+ if (lastSlashIndexInPath !== -1) {
+ autoCompleteStartIndex += lastSlashIndexInPath + 1;
+ }
+ buffer.replaceRangeByOffset(
+ autoCompleteStartIndex,
+ buffer.text.length,
+ suggestion,
+ );
+ }
+ resetCompletionState();
+ },
+ [resetCompletionState, buffer, completionSuggestions, slashCommands],
+ );
+
+ // Handle clipboard image pasting with Ctrl+V
+ const handleClipboardImage = useCallback(async () => {
+ try {
+ if (await clipboardHasImage()) {
+ const imagePath = await saveClipboardImage(config.getTargetDir());
+ if (imagePath) {
+ // Clean up old images
+ cleanupOldClipboardImages(config.getTargetDir()).catch(() => {
+ // Ignore cleanup errors
+ });
+
+ // Get relative path from current directory
+ const relativePath = path.relative(config.getTargetDir(), imagePath);
+
+ // Insert @path reference at cursor position
+ const insertText = `@${relativePath}`;
+ const currentText = buffer.text;
+ const [row, col] = buffer.cursor;
+
+ // Calculate offset from row/col
+ let offset = 0;
+ for (let i = 0; i < row; i++) {
+ offset += buffer.lines[i].length + 1; // +1 for newline
+ }
+ offset += col;
+
+ // Add spaces around the path if needed
+ let textToInsert = insertText;
+ const charBefore = offset > 0 ? currentText[offset - 1] : '';
+ const charAfter =
+ offset < currentText.length ? currentText[offset] : '';
+
+ if (charBefore && charBefore !== ' ' && charBefore !== '\n') {
+ textToInsert = ' ' + textToInsert;
+ }
+ if (!charAfter || (charAfter !== ' ' && charAfter !== '\n')) {
+ textToInsert = textToInsert + ' ';
+ }
+
+ // Insert at cursor position
+ buffer.replaceRangeByOffset(offset, offset, textToInsert);
+ }
+ }
+ } catch (error) {
+ console.error('Error handling clipboard image:', error);
+ }
+ }, [buffer, config]);
+
+ const handleInput = useCallback(
+ (key: Key) => {
+ if (!focus) {
+ return;
+ }
+
+ if (
+ key.sequence === '!' &&
+ buffer.text === '' &&
+ !completion.showSuggestions
+ ) {
+ setShellModeActive(!shellModeActive);
+ buffer.setText(''); // Clear the '!' from input
+ return;
+ }
+
+ if (key.name === 'escape') {
+ if (shellModeActive) {
+ setShellModeActive(false);
+ return;
+ }
+
+ if (completion.showSuggestions) {
+ completion.resetCompletionState();
+ return;
+ }
+ }
+
+ if (key.ctrl && key.name === 'l') {
+ onClearScreen();
+ return;
+ }
+
+ if (completion.showSuggestions) {
+ if (key.name === 'up') {
+ completion.navigateUp();
+ return;
+ }
+ if (key.name === 'down') {
+ completion.navigateDown();
+ return;
+ }
+
+ if (key.name === 'tab' || (key.name === 'return' && !key.ctrl)) {
+ if (completion.suggestions.length > 0) {
+ const targetIndex =
+ completion.activeSuggestionIndex === -1
+ ? 0 // Default to the first if none is active
+ : completion.activeSuggestionIndex;
+ if (targetIndex < completion.suggestions.length) {
+ handleAutocomplete(targetIndex);
+ }
+ }
+ return;
+ }
+ } else {
+ if (!shellModeActive) {
+ if (key.ctrl && key.name === 'p') {
+ inputHistory.navigateUp();
+ return;
+ }
+ if (key.ctrl && key.name === 'n') {
+ inputHistory.navigateDown();
+ return;
+ }
+ // Handle arrow-up/down for history on single-line or at edges
+ if (
+ key.name === 'up' &&
+ (buffer.allVisualLines.length === 1 ||
+ (buffer.visualCursor[0] === 0 && buffer.visualScrollRow === 0))
+ ) {
+ inputHistory.navigateUp();
+ return;
+ }
+ if (
+ key.name === 'down' &&
+ (buffer.allVisualLines.length === 1 ||
+ buffer.visualCursor[0] === buffer.allVisualLines.length - 1)
+ ) {
+ inputHistory.navigateDown();
+ return;
+ }
+ } else {
+ // Shell History Navigation
+ if (key.name === 'up') {
+ const prevCommand = shellHistory.getPreviousCommand();
+ if (prevCommand !== null) buffer.setText(prevCommand);
+ return;
+ }
+ if (key.name === 'down') {
+ const nextCommand = shellHistory.getNextCommand();
+ if (nextCommand !== null) buffer.setText(nextCommand);
+ return;
+ }
+ }
+
+ if (key.name === 'return' && !key.ctrl && !key.meta && !key.paste) {
+ if (buffer.text.trim()) {
+ const [row, col] = buffer.cursor;
+ const line = buffer.lines[row];
+ const charBefore = col > 0 ? cpSlice(line, col - 1, col) : '';
+ if (charBefore === '\\') {
+ buffer.backspace();
+ buffer.newline();
+ } else {
+ handleSubmitAndClear(buffer.text);
+ }
+ }
+ return;
+ }
+ }
+
+ // Newline insertion
+ if (key.name === 'return' && (key.ctrl || key.meta || key.paste)) {
+ buffer.newline();
+ return;
+ }
+
+ // Ctrl+A (Home) / Ctrl+E (End)
+ if (key.ctrl && key.name === 'a') {
+ buffer.move('home');
+ return;
+ }
+ if (key.ctrl && key.name === 'e') {
+ buffer.move('end');
+ return;
+ }
+
+ // Kill line commands
+ if (key.ctrl && key.name === 'k') {
+ buffer.killLineRight();
+ return;
+ }
+ if (key.ctrl && key.name === 'u') {
+ buffer.killLineLeft();
+ return;
+ }
+
+ // External editor
+ const isCtrlX = key.ctrl && (key.name === 'x' || key.sequence === '\x18');
+ if (isCtrlX) {
+ buffer.openInExternalEditor();
+ return;
+ }
+
+ // Ctrl+V for clipboard image paste
+ if (key.ctrl && key.name === 'v') {
+ handleClipboardImage();
+ return;
+ }
+
+ // Fallback to the text buffer's default input handling for all other keys
+ buffer.handleInput(key);
+ },
+ [
+ focus,
+ buffer,
+ completion,
+ shellModeActive,
+ setShellModeActive,
+ onClearScreen,
+ inputHistory,
+ handleAutocomplete,
+ handleSubmitAndClear,
+ shellHistory,
+ handleClipboardImage,
+ ],
+ );
+
+ useKeypress(handleInput, { isActive: focus });
+
+ const linesToRender = buffer.viewportVisualLines;
+ const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
+ buffer.visualCursor;
+ const scrollVisualRow = buffer.visualScrollRow;
+
+ return (
+ <>
+
+
+ {shellModeActive ? '! ' : '> '}
+
+
+ {buffer.text.length === 0 && placeholder ? (
+ focus ? (
+
+ {chalk.inverse(placeholder.slice(0, 1))}
+ {placeholder.slice(1)}
+
+ ) : (
+ {placeholder}
+ )
+ ) : (
+ linesToRender.map((lineText, visualIdxInRenderedSet) => {
+ const cursorVisualRow = cursorVisualRowAbsolute - scrollVisualRow;
+ let display = cpSlice(lineText, 0, inputWidth);
+ const currentVisualWidth = stringWidth(display);
+ if (currentVisualWidth < inputWidth) {
+ display = display + ' '.repeat(inputWidth - currentVisualWidth);
+ }
+
+ if (visualIdxInRenderedSet === cursorVisualRow) {
+ const relativeVisualColForHighlight = cursorVisualColAbsolute;
+
+ if (relativeVisualColForHighlight >= 0) {
+ if (relativeVisualColForHighlight < cpLen(display)) {
+ const charToHighlight =
+ cpSlice(
+ display,
+ relativeVisualColForHighlight,
+ relativeVisualColForHighlight + 1,
+ ) || ' ';
+ const highlighted = chalk.inverse(charToHighlight);
+ display =
+ cpSlice(display, 0, relativeVisualColForHighlight) +
+ highlighted +
+ cpSlice(display, relativeVisualColForHighlight + 1);
+ } else if (
+ relativeVisualColForHighlight === cpLen(display) &&
+ cpLen(display) === inputWidth
+ ) {
+ display = display + chalk.inverse(' ');
+ }
+ }
+ }
+ return (
+ {display}
+ );
+ })
+ )}
+
+
+ {completion.showSuggestions && (
+
+
+
+ )}
+ >
+ );
+};
diff --git a/packages/cli/src/ui/components/LoadingIndicator.test.tsx b/packages/cli/src/ui/components/LoadingIndicator.test.tsx
new file mode 100644
index 00000000..9e647208
--- /dev/null
+++ b/packages/cli/src/ui/components/LoadingIndicator.test.tsx
@@ -0,0 +1,226 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { render } from 'ink-testing-library';
+import { Text } from 'ink';
+import { LoadingIndicator } from './LoadingIndicator.js';
+import { StreamingContext } from '../contexts/StreamingContext.js';
+import { StreamingState } from '../types.js';
+import { vi } from 'vitest';
+
+// Mock GeminiRespondingSpinner
+vi.mock('./GeminiRespondingSpinner.js', () => ({
+ GeminiRespondingSpinner: ({
+ nonRespondingDisplay,
+ }: {
+ nonRespondingDisplay?: string;
+ }) => {
+ const streamingState = React.useContext(StreamingContext)!;
+ if (streamingState === StreamingState.Responding) {
+ return MockRespondingSpinner;
+ } else if (nonRespondingDisplay) {
+ return {nonRespondingDisplay};
+ }
+ return null;
+ },
+}));
+
+const renderWithContext = (
+ ui: React.ReactElement,
+ streamingStateValue: StreamingState,
+) => {
+ const contextValue: StreamingState = streamingStateValue;
+ return render(
+
+ {ui}
+ ,
+ );
+};
+
+describe('', () => {
+ const defaultProps = {
+ currentLoadingPhrase: 'Loading...',
+ elapsedTime: 5,
+ };
+
+ it('should not render when streamingState is Idle', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ expect(lastFrame()).toBe('');
+ });
+
+ it('should render spinner, phrase, and time when streamingState is Responding', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding,
+ );
+ const output = lastFrame();
+ expect(output).toContain('MockRespondingSpinner');
+ expect(output).toContain('Loading...');
+ expect(output).toContain('(esc to cancel, 5s)');
+ });
+
+ it('should render spinner (static), phrase but no time/cancel when streamingState is WaitingForConfirmation', () => {
+ const props = {
+ currentLoadingPhrase: 'Confirm action',
+ elapsedTime: 10,
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.WaitingForConfirmation,
+ );
+ const output = lastFrame();
+ expect(output).toContain('โ '); // Static char for WaitingForConfirmation
+ expect(output).toContain('Confirm action');
+ expect(output).not.toContain('(esc to cancel)');
+ expect(output).not.toContain(', 10s');
+ });
+
+ it('should display the currentLoadingPhrase correctly', () => {
+ const props = {
+ currentLoadingPhrase: 'Processing data...',
+ elapsedTime: 3,
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding,
+ );
+ expect(lastFrame()).toContain('Processing data...');
+ });
+
+ it('should display the elapsedTime correctly when Responding', () => {
+ const props = {
+ currentLoadingPhrase: 'Working...',
+ elapsedTime: 60,
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding,
+ );
+ expect(lastFrame()).toContain('(esc to cancel, 1m)');
+ });
+
+ it('should display the elapsedTime correctly in human-readable format', () => {
+ const props = {
+ currentLoadingPhrase: 'Working...',
+ elapsedTime: 125,
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding,
+ );
+ expect(lastFrame()).toContain('(esc to cancel, 2m 5s)');
+ });
+
+ it('should render rightContent when provided', () => {
+ const rightContent = Extra Info;
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding,
+ );
+ expect(lastFrame()).toContain('Extra Info');
+ });
+
+ it('should transition correctly between states using rerender', () => {
+ const { lastFrame, rerender } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ expect(lastFrame()).toBe(''); // Initial: Idle
+
+ // Transition to Responding
+ rerender(
+
+
+ ,
+ );
+ let output = lastFrame();
+ expect(output).toContain('MockRespondingSpinner');
+ expect(output).toContain('Now Responding');
+ expect(output).toContain('(esc to cancel, 2s)');
+
+ // Transition to WaitingForConfirmation
+ rerender(
+
+
+ ,
+ );
+ output = lastFrame();
+ expect(output).toContain('โ ');
+ expect(output).toContain('Please Confirm');
+ expect(output).not.toContain('(esc to cancel)');
+ expect(output).not.toContain(', 15s');
+
+ // Transition back to Idle
+ rerender(
+
+
+ ,
+ );
+ expect(lastFrame()).toBe('');
+ });
+
+ it('should display fallback phrase if thought is empty', () => {
+ const props = {
+ thought: null,
+ currentLoadingPhrase: 'Loading...',
+ elapsedTime: 5,
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding,
+ );
+ const output = lastFrame();
+ expect(output).toContain('Loading...');
+ });
+
+ it('should display the subject of a thought', () => {
+ const props = {
+ thought: {
+ subject: 'Thinking about something...',
+ description: 'and other stuff.',
+ },
+ elapsedTime: 5,
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding,
+ );
+ const output = lastFrame();
+ expect(output).toBeDefined();
+ if (output) {
+ expect(output).toContain('Thinking about something...');
+ expect(output).not.toContain('and other stuff.');
+ }
+ });
+
+ it('should prioritize thought.subject over currentLoadingPhrase', () => {
+ const props = {
+ thought: {
+ subject: 'This should be displayed',
+ description: 'A description',
+ },
+ currentLoadingPhrase: 'This should not be displayed',
+ elapsedTime: 5,
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding,
+ );
+ const output = lastFrame();
+ expect(output).toContain('This should be displayed');
+ expect(output).not.toContain('This should not be displayed');
+ });
+});
diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx
new file mode 100644
index 00000000..7a51256c
--- /dev/null
+++ b/packages/cli/src/ui/components/LoadingIndicator.tsx
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ThoughtSummary } from '@qwen/qwen-code-core';
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { useStreamingContext } from '../contexts/StreamingContext.js';
+import { StreamingState } from '../types.js';
+import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
+import { formatDuration } from '../utils/formatters.js';
+
+interface LoadingIndicatorProps {
+ currentLoadingPhrase?: string;
+ elapsedTime: number;
+ rightContent?: React.ReactNode;
+ thought?: ThoughtSummary | null;
+}
+
+export const LoadingIndicator: React.FC = ({
+ currentLoadingPhrase,
+ elapsedTime,
+ rightContent,
+ thought,
+}) => {
+ const streamingState = useStreamingContext();
+
+ if (streamingState === StreamingState.Idle) {
+ return null;
+ }
+
+ const primaryText = thought?.subject || currentLoadingPhrase;
+
+ return (
+
+ {/* Main loading line */}
+
+
+
+
+ {primaryText && {primaryText}}
+
+ {streamingState === StreamingState.WaitingForConfirmation
+ ? ''
+ : ` (esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})`}
+
+ {/* Spacer */}
+ {rightContent && {rightContent}}
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/MemoryUsageDisplay.tsx b/packages/cli/src/ui/components/MemoryUsageDisplay.tsx
new file mode 100644
index 00000000..d768445c
--- /dev/null
+++ b/packages/cli/src/ui/components/MemoryUsageDisplay.tsx
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useEffect, useState } from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import process from 'node:process';
+import { formatMemoryUsage } from '../utils/formatters.js';
+
+export const MemoryUsageDisplay: React.FC = () => {
+ const [memoryUsage, setMemoryUsage] = useState('');
+ const [memoryUsageColor, setMemoryUsageColor] = useState(Colors.Gray);
+
+ useEffect(() => {
+ const updateMemory = () => {
+ const usage = process.memoryUsage().rss;
+ setMemoryUsage(formatMemoryUsage(usage));
+ setMemoryUsageColor(
+ usage >= 2 * 1024 * 1024 * 1024 ? Colors.AccentRed : Colors.Gray,
+ );
+ };
+ const intervalId = setInterval(updateMemory, 2000);
+ updateMemory(); // Initial update
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+ |
+ {memoryUsage}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx
new file mode 100644
index 00000000..57382d91
--- /dev/null
+++ b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx
@@ -0,0 +1,239 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { ModelStatsDisplay } from './ModelStatsDisplay.js';
+import * as SessionContext from '../contexts/SessionContext.js';
+import { SessionMetrics } from '../contexts/SessionContext.js';
+
+// Mock the context to provide controlled data for testing
+vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useSessionStats: vi.fn(),
+ };
+});
+
+const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
+
+const renderWithMockedStats = (metrics: SessionMetrics) => {
+ useSessionStatsMock.mockReturnValue({
+ stats: {
+ sessionStartTime: new Date(),
+ metrics,
+ lastPromptTokenCount: 0,
+ promptCount: 5,
+ },
+
+ getPromptCount: () => 5,
+ startNewPrompt: vi.fn(),
+ });
+
+ return render();
+};
+
+describe('', () => {
+ it('should render "no API calls" message when there are no active models', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {},
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ });
+
+ expect(lastFrame()).toContain(
+ 'No API calls have been made in this session.',
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('should not display conditional rows if no model has data for them', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {
+ 'gemini-2.5-pro': {
+ api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
+ tokens: {
+ prompt: 10,
+ candidates: 20,
+ total: 30,
+ cached: 0,
+ thoughts: 0,
+ tool: 0,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ });
+
+ const output = lastFrame();
+ expect(output).not.toContain('Cached');
+ expect(output).not.toContain('Thoughts');
+ expect(output).not.toContain('Tool');
+ expect(output).toMatchSnapshot();
+ });
+
+ it('should display conditional rows if at least one model has data', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {
+ 'gemini-2.5-pro': {
+ api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
+ tokens: {
+ prompt: 10,
+ candidates: 20,
+ total: 30,
+ cached: 5,
+ thoughts: 2,
+ tool: 0,
+ },
+ },
+ 'gemini-2.5-flash': {
+ api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 50 },
+ tokens: {
+ prompt: 5,
+ candidates: 10,
+ total: 15,
+ cached: 0,
+ thoughts: 0,
+ tool: 3,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ });
+
+ const output = lastFrame();
+ expect(output).toContain('Cached');
+ expect(output).toContain('Thoughts');
+ expect(output).toContain('Tool');
+ expect(output).toMatchSnapshot();
+ });
+
+ it('should display stats for multiple models correctly', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {
+ 'gemini-2.5-pro': {
+ api: { totalRequests: 10, totalErrors: 1, totalLatencyMs: 1000 },
+ tokens: {
+ prompt: 100,
+ candidates: 200,
+ total: 300,
+ cached: 50,
+ thoughts: 10,
+ tool: 5,
+ },
+ },
+ 'gemini-2.5-flash': {
+ api: { totalRequests: 20, totalErrors: 2, totalLatencyMs: 500 },
+ tokens: {
+ prompt: 200,
+ candidates: 400,
+ total: 600,
+ cached: 100,
+ thoughts: 20,
+ tool: 10,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ });
+
+ const output = lastFrame();
+ expect(output).toContain('gemini-2.5-pro');
+ expect(output).toContain('gemini-2.5-flash');
+ expect(output).toMatchSnapshot();
+ });
+
+ it('should handle large values without wrapping or overlapping', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {
+ 'gemini-2.5-pro': {
+ api: {
+ totalRequests: 999999999,
+ totalErrors: 123456789,
+ totalLatencyMs: 9876,
+ },
+ tokens: {
+ prompt: 987654321,
+ candidates: 123456789,
+ total: 999999999,
+ cached: 123456789,
+ thoughts: 111111111,
+ tool: 222222222,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ });
+
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('should display a single model correctly', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {
+ 'gemini-2.5-pro': {
+ api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
+ tokens: {
+ prompt: 10,
+ candidates: 20,
+ total: 30,
+ cached: 5,
+ thoughts: 2,
+ tool: 1,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ });
+
+ const output = lastFrame();
+ expect(output).toContain('gemini-2.5-pro');
+ expect(output).not.toContain('gemini-2.5-flash');
+ expect(output).toMatchSnapshot();
+ });
+});
diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.tsx
new file mode 100644
index 00000000..1911e757
--- /dev/null
+++ b/packages/cli/src/ui/components/ModelStatsDisplay.tsx
@@ -0,0 +1,197 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { formatDuration } from '../utils/formatters.js';
+import {
+ calculateAverageLatency,
+ calculateCacheHitRate,
+ calculateErrorRate,
+} from '../utils/computeStats.js';
+import { useSessionStats, ModelMetrics } from '../contexts/SessionContext.js';
+
+const METRIC_COL_WIDTH = 28;
+const MODEL_COL_WIDTH = 22;
+
+interface StatRowProps {
+ title: string;
+ values: Array;
+ isSubtle?: boolean;
+ isSection?: boolean;
+}
+
+const StatRow: React.FC = ({
+ title,
+ values,
+ isSubtle = false,
+ isSection = false,
+}) => (
+
+
+
+ {isSubtle ? ` โณ ${title}` : title}
+
+
+ {values.map((value, index) => (
+
+ {value}
+
+ ))}
+
+);
+
+export const ModelStatsDisplay: React.FC = () => {
+ const { stats } = useSessionStats();
+ const { models } = stats.metrics;
+ const activeModels = Object.entries(models).filter(
+ ([, metrics]) => metrics.api.totalRequests > 0,
+ );
+
+ if (activeModels.length === 0) {
+ return (
+
+ No API calls have been made in this session.
+
+ );
+ }
+
+ const modelNames = activeModels.map(([name]) => name);
+
+ const getModelValues = (
+ getter: (metrics: ModelMetrics) => string | React.ReactElement,
+ ) => activeModels.map(([, metrics]) => getter(metrics));
+
+ const hasThoughts = activeModels.some(
+ ([, metrics]) => metrics.tokens.thoughts > 0,
+ );
+ const hasTool = activeModels.some(([, metrics]) => metrics.tokens.tool > 0);
+ const hasCached = activeModels.some(
+ ([, metrics]) => metrics.tokens.cached > 0,
+ );
+
+ return (
+
+
+ Model Stats For Nerds
+
+
+
+ {/* Header */}
+
+
+ Metric
+
+ {modelNames.map((name) => (
+
+ {name}
+
+ ))}
+
+
+ {/* Divider */}
+
+
+ {/* API Section */}
+
+ m.api.totalRequests.toLocaleString())}
+ />
+ {
+ const errorRate = calculateErrorRate(m);
+ return (
+ 0 ? Colors.AccentRed : Colors.Foreground
+ }
+ >
+ {m.api.totalErrors.toLocaleString()} ({errorRate.toFixed(1)}%)
+
+ );
+ })}
+ />
+ {
+ const avgLatency = calculateAverageLatency(m);
+ return formatDuration(avgLatency);
+ })}
+ />
+
+
+
+ {/* Tokens Section */}
+
+ (
+
+ {m.tokens.total.toLocaleString()}
+
+ ))}
+ />
+ m.tokens.prompt.toLocaleString())}
+ />
+ {hasCached && (
+ {
+ const cacheHitRate = calculateCacheHitRate(m);
+ return (
+
+ {m.tokens.cached.toLocaleString()} ({cacheHitRate.toFixed(1)}%)
+
+ );
+ })}
+ />
+ )}
+ {hasThoughts && (
+ m.tokens.thoughts.toLocaleString())}
+ />
+ )}
+ {hasTool && (
+ m.tokens.tool.toLocaleString())}
+ />
+ )}
+ m.tokens.candidates.toLocaleString())}
+ />
+
+ );
+};
diff --git a/packages/cli/src/ui/components/OpenAIKeyPrompt.test.tsx b/packages/cli/src/ui/components/OpenAIKeyPrompt.test.tsx
new file mode 100644
index 00000000..77353e9f
--- /dev/null
+++ b/packages/cli/src/ui/components/OpenAIKeyPrompt.test.tsx
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { OpenAIKeyPrompt } from './OpenAIKeyPrompt.js';
+
+describe('OpenAIKeyPrompt', () => {
+ it('should render the prompt correctly', () => {
+ const onSubmit = vi.fn();
+ const onCancel = vi.fn();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('OpenAI Configuration Required');
+ expect(lastFrame()).toContain('https://platform.openai.com/api-keys');
+ expect(lastFrame()).toContain(
+ 'Press Enter to continue, Tab/โโ to navigate, Esc to cancel',
+ );
+ });
+
+ it('should show the component with proper styling', () => {
+ const onSubmit = vi.fn();
+ const onCancel = vi.fn();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ const output = lastFrame();
+ expect(output).toContain('OpenAI Configuration Required');
+ expect(output).toContain('API Key:');
+ expect(output).toContain('Base URL:');
+ expect(output).toContain('Model:');
+ expect(output).toContain(
+ 'Press Enter to continue, Tab/โโ to navigate, Esc to cancel',
+ );
+ });
+
+ it('should handle paste with control characters', async () => {
+ const onSubmit = vi.fn();
+ const onCancel = vi.fn();
+
+ const { stdin } = render(
+ ,
+ );
+
+ // Simulate paste with control characters
+ const pasteWithControlChars = '\x1b[200~sk-test123\x1b[201~';
+ stdin.write(pasteWithControlChars);
+
+ // Wait a bit for processing
+ await new Promise((resolve) => setTimeout(resolve, 50));
+
+ // The component should have filtered out the control characters
+ // and only kept 'sk-test123'
+ expect(onSubmit).not.toHaveBeenCalled(); // Should not submit yet
+ });
+});
diff --git a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx
new file mode 100644
index 00000000..bf9f4bff
--- /dev/null
+++ b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx
@@ -0,0 +1,197 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { Colors } from '../colors.js';
+
+interface OpenAIKeyPromptProps {
+ onSubmit: (apiKey: string, baseUrl: string, model: string) => void;
+ onCancel: () => void;
+}
+
+export function OpenAIKeyPrompt({
+ onSubmit,
+ onCancel,
+}: OpenAIKeyPromptProps): React.JSX.Element {
+ const [apiKey, setApiKey] = useState('');
+ const [baseUrl, setBaseUrl] = useState('');
+ const [model, setModel] = useState('');
+ const [currentField, setCurrentField] = useState<
+ 'apiKey' | 'baseUrl' | 'model'
+ >('apiKey');
+
+ useInput((input, key) => {
+ // ่ฟๆปค็ฒ่ดด็ธๅ
ณ็ๆงๅถๅบๅ
+ let cleanInput = (input || '')
+ // ่ฟๆปค ESC ๅผๅคด็ๆงๅถๅบๅ๏ผๅฆ \u001b[200~ใ\u001b[201~ ็ญ๏ผ
+ .replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') // eslint-disable-line no-control-regex
+ // ่ฟๆปค็ฒ่ดดๅผๅงๆ ่ฎฐ [200~
+ .replace(/\[200~/g, '')
+ // ่ฟๆปค็ฒ่ดด็ปๆๆ ่ฎฐ [201~
+ .replace(/\[201~/g, '')
+ // ่ฟๆปคๅ็ฌ็ [ ๅ ~ ๅญ็ฌฆ๏ผๅฏ่ฝๆฏ็ฒ่ดดๆ ่ฎฐ็ๆฎ็๏ผ
+ .replace(/^\[|~$/g, '');
+
+ // ๅ่ฟๆปคๆๆไธๅฏ่งๅญ็ฌฆ๏ผASCII < 32๏ผ้คไบๅ่ฝฆๆข่ก๏ผ
+ cleanInput = cleanInput
+ .split('')
+ .filter((ch) => ch.charCodeAt(0) >= 32)
+ .join('');
+
+ if (cleanInput.length > 0) {
+ if (currentField === 'apiKey') {
+ setApiKey((prev) => prev + cleanInput);
+ } else if (currentField === 'baseUrl') {
+ setBaseUrl((prev) => prev + cleanInput);
+ } else if (currentField === 'model') {
+ setModel((prev) => prev + cleanInput);
+ }
+ return;
+ }
+
+ // ๆฃๆฅๆฏๅฆๆฏ Enter ้ฎ๏ผ้่ฟๆฃๆฅ่พๅ
ฅๆฏๅฆๅ
ๅซๆข่ก็ฌฆ๏ผ
+ if (input.includes('\n') || input.includes('\r')) {
+ if (currentField === 'apiKey') {
+ // ๅ
่ฎธ็ฉบ API key ่ทณ่ฝฌๅฐไธไธไธชๅญๆฎต๏ผ่ฎฉ็จๆท็จๅๅฏไปฅ่ฟๅไฟฎๆน
+ setCurrentField('baseUrl');
+ return;
+ } else if (currentField === 'baseUrl') {
+ setCurrentField('model');
+ return;
+ } else if (currentField === 'model') {
+ // ๅชๆๅจๆไบคๆถๆๆฃๆฅ API key ๆฏๅฆไธบ็ฉบ
+ if (apiKey.trim()) {
+ onSubmit(apiKey.trim(), baseUrl.trim(), model.trim());
+ } else {
+ // ๅฆๆ API key ไธบ็ฉบ๏ผๅๅฐ API key ๅญๆฎต
+ setCurrentField('apiKey');
+ }
+ }
+ return;
+ }
+
+ if (key.escape) {
+ onCancel();
+ return;
+ }
+
+ // Handle Tab key for field navigation
+ if (key.tab) {
+ if (currentField === 'apiKey') {
+ setCurrentField('baseUrl');
+ } else if (currentField === 'baseUrl') {
+ setCurrentField('model');
+ } else if (currentField === 'model') {
+ setCurrentField('apiKey');
+ }
+ return;
+ }
+
+ // Handle arrow keys for field navigation
+ if (key.upArrow) {
+ if (currentField === 'baseUrl') {
+ setCurrentField('apiKey');
+ } else if (currentField === 'model') {
+ setCurrentField('baseUrl');
+ }
+ return;
+ }
+
+ if (key.downArrow) {
+ if (currentField === 'apiKey') {
+ setCurrentField('baseUrl');
+ } else if (currentField === 'baseUrl') {
+ setCurrentField('model');
+ }
+ return;
+ }
+
+ // Handle backspace - check both key.backspace and delete key
+ if (key.backspace || key.delete) {
+ if (currentField === 'apiKey') {
+ setApiKey((prev) => prev.slice(0, -1));
+ } else if (currentField === 'baseUrl') {
+ setBaseUrl((prev) => prev.slice(0, -1));
+ } else if (currentField === 'model') {
+ setModel((prev) => prev.slice(0, -1));
+ }
+ return;
+ }
+ });
+
+ return (
+
+
+ OpenAI Configuration Required
+
+
+
+ Please enter your OpenAI configuration. You can get an API key from{' '}
+
+ https://platform.openai.com/api-keys
+
+
+
+
+
+
+ API Key:
+
+
+
+
+ {currentField === 'apiKey' ? '> ' : ' '}
+ {apiKey || ' '}
+
+
+
+
+
+
+ Base URL:
+
+
+
+
+ {currentField === 'baseUrl' ? '> ' : ' '}
+ {baseUrl}
+
+
+
+
+
+
+ Model:
+
+
+
+
+ {currentField === 'model' ? '> ' : ' '}
+ {model}
+
+
+
+
+
+ Press Enter to continue, Tab/โโ to navigate, Esc to cancel
+
+
+
+ );
+}
diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx
new file mode 100644
index 00000000..38400caf
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx
@@ -0,0 +1,71 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
+import * as SessionContext from '../contexts/SessionContext.js';
+import { SessionMetrics } from '../contexts/SessionContext.js';
+
+vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useSessionStats: vi.fn(),
+ };
+});
+
+const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
+
+const renderWithMockedStats = (metrics: SessionMetrics) => {
+ useSessionStatsMock.mockReturnValue({
+ stats: {
+ sessionStartTime: new Date(),
+ metrics,
+ lastPromptTokenCount: 0,
+ promptCount: 5,
+ },
+
+ getPromptCount: () => 5,
+ startNewPrompt: vi.fn(),
+ });
+
+ return render();
+};
+
+describe('', () => {
+ it('renders the summary display with a title', () => {
+ const metrics: SessionMetrics = {
+ models: {
+ 'gemini-2.5-pro': {
+ api: { totalRequests: 10, totalErrors: 1, totalLatencyMs: 50234 },
+ tokens: {
+ prompt: 1000,
+ candidates: 2000,
+ total: 3500,
+ cached: 500,
+ thoughts: 300,
+ tool: 200,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ const { lastFrame } = renderWithMockedStats(metrics);
+ const output = lastFrame();
+
+ expect(output).toContain('Agent powering down. Goodbye!');
+ expect(output).toMatchSnapshot();
+ });
+});
diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx
new file mode 100644
index 00000000..34e3cc72
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { StatsDisplay } from './StatsDisplay.js';
+
+interface SessionSummaryDisplayProps {
+ duration: string;
+}
+
+export const SessionSummaryDisplay: React.FC = ({
+ duration,
+}) => (
+
+);
diff --git a/packages/cli/src/ui/components/ShellModeIndicator.tsx b/packages/cli/src/ui/components/ShellModeIndicator.tsx
new file mode 100644
index 00000000..f5b11b24
--- /dev/null
+++ b/packages/cli/src/ui/components/ShellModeIndicator.tsx
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+
+export const ShellModeIndicator: React.FC = () => (
+
+
+ shell mode enabled
+ (esc to disable)
+
+
+);
diff --git a/packages/cli/src/ui/components/ShowMoreLines.tsx b/packages/cli/src/ui/components/ShowMoreLines.tsx
new file mode 100644
index 00000000..41232d94
--- /dev/null
+++ b/packages/cli/src/ui/components/ShowMoreLines.tsx
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Box, Text } from 'ink';
+import { useOverflowState } from '../contexts/OverflowContext.js';
+import { useStreamingContext } from '../contexts/StreamingContext.js';
+import { StreamingState } from '../types.js';
+import { Colors } from '../colors.js';
+
+interface ShowMoreLinesProps {
+ constrainHeight: boolean;
+}
+
+export const ShowMoreLines = ({ constrainHeight }: ShowMoreLinesProps) => {
+ const overflowState = useOverflowState();
+ const streamingState = useStreamingContext();
+
+ if (
+ overflowState === undefined ||
+ overflowState.overflowingIds.size === 0 ||
+ !constrainHeight ||
+ !(
+ streamingState === StreamingState.Idle ||
+ streamingState === StreamingState.WaitingForConfirmation
+ )
+ ) {
+ return null;
+ }
+
+ return (
+
+
+ Press ctrl-s to show more lines
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx
new file mode 100644
index 00000000..a0ed3858
--- /dev/null
+++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx
@@ -0,0 +1,311 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { StatsDisplay } from './StatsDisplay.js';
+import * as SessionContext from '../contexts/SessionContext.js';
+import { SessionMetrics } from '../contexts/SessionContext.js';
+
+// Mock the context to provide controlled data for testing
+vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useSessionStats: vi.fn(),
+ };
+});
+
+const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
+
+const renderWithMockedStats = (metrics: SessionMetrics) => {
+ useSessionStatsMock.mockReturnValue({
+ stats: {
+ sessionStartTime: new Date(),
+ metrics,
+ lastPromptTokenCount: 0,
+ promptCount: 5,
+ },
+
+ getPromptCount: () => 5,
+ startNewPrompt: vi.fn(),
+ });
+
+ return render();
+};
+
+describe('', () => {
+ it('renders only the Performance section in its zero state', () => {
+ const zeroMetrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ const { lastFrame } = renderWithMockedStats(zeroMetrics);
+ const output = lastFrame();
+
+ expect(output).toContain('Performance');
+ expect(output).not.toContain('Interaction Summary');
+ expect(output).not.toContain('Efficiency & Optimizations');
+ expect(output).not.toContain('Model'); // The table header
+ expect(output).toMatchSnapshot();
+ });
+
+ it('renders a table with two models correctly', () => {
+ const metrics: SessionMetrics = {
+ models: {
+ 'gemini-2.5-pro': {
+ api: { totalRequests: 3, totalErrors: 0, totalLatencyMs: 15000 },
+ tokens: {
+ prompt: 1000,
+ candidates: 2000,
+ total: 43234,
+ cached: 500,
+ thoughts: 100,
+ tool: 50,
+ },
+ },
+ 'gemini-2.5-flash': {
+ api: { totalRequests: 5, totalErrors: 1, totalLatencyMs: 4500 },
+ tokens: {
+ prompt: 25000,
+ candidates: 15000,
+ total: 150000000,
+ cached: 10000,
+ thoughts: 2000,
+ tool: 1000,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ const { lastFrame } = renderWithMockedStats(metrics);
+ const output = lastFrame();
+
+ expect(output).toContain('gemini-2.5-pro');
+ expect(output).toContain('gemini-2.5-flash');
+ expect(output).toContain('1,000');
+ expect(output).toContain('25,000');
+ expect(output).toMatchSnapshot();
+ });
+
+ it('renders all sections when all data is present', () => {
+ const metrics: SessionMetrics = {
+ models: {
+ 'gemini-2.5-pro': {
+ api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
+ tokens: {
+ prompt: 100,
+ candidates: 100,
+ total: 250,
+ cached: 50,
+ thoughts: 0,
+ tool: 0,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 2,
+ totalSuccess: 1,
+ totalFail: 1,
+ totalDurationMs: 123,
+ totalDecisions: { accept: 1, reject: 0, modify: 0 },
+ byName: {
+ 'test-tool': {
+ count: 2,
+ success: 1,
+ fail: 1,
+ durationMs: 123,
+ decisions: { accept: 1, reject: 0, modify: 0 },
+ },
+ },
+ },
+ };
+
+ const { lastFrame } = renderWithMockedStats(metrics);
+ const output = lastFrame();
+
+ expect(output).toContain('Performance');
+ expect(output).toContain('Interaction Summary');
+ expect(output).toContain('User Agreement');
+ expect(output).toContain('Savings Highlight');
+ expect(output).toContain('gemini-2.5-pro');
+ expect(output).toMatchSnapshot();
+ });
+
+ describe('Conditional Rendering Tests', () => {
+ it('hides User Agreement when no decisions are made', () => {
+ const metrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 2,
+ totalSuccess: 1,
+ totalFail: 1,
+ totalDurationMs: 123,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 }, // No decisions
+ byName: {
+ 'test-tool': {
+ count: 2,
+ success: 1,
+ fail: 1,
+ durationMs: 123,
+ decisions: { accept: 0, reject: 0, modify: 0 },
+ },
+ },
+ },
+ };
+
+ const { lastFrame } = renderWithMockedStats(metrics);
+ const output = lastFrame();
+
+ expect(output).toContain('Interaction Summary');
+ expect(output).toContain('Success Rate');
+ expect(output).not.toContain('User Agreement');
+ expect(output).toMatchSnapshot();
+ });
+
+ it('hides Efficiency section when cache is not used', () => {
+ const metrics: SessionMetrics = {
+ models: {
+ 'gemini-2.5-pro': {
+ api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
+ tokens: {
+ prompt: 100,
+ candidates: 100,
+ total: 200,
+ cached: 0,
+ thoughts: 0,
+ tool: 0,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ const { lastFrame } = renderWithMockedStats(metrics);
+ const output = lastFrame();
+
+ expect(output).not.toContain('Efficiency & Optimizations');
+ expect(output).toMatchSnapshot();
+ });
+ });
+
+ describe('Conditional Color Tests', () => {
+ it('renders success rate in green for high values', () => {
+ const metrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 10,
+ totalSuccess: 10,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+ const { lastFrame } = renderWithMockedStats(metrics);
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders success rate in yellow for medium values', () => {
+ const metrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 10,
+ totalSuccess: 9,
+ totalFail: 1,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+ const { lastFrame } = renderWithMockedStats(metrics);
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('renders success rate in red for low values', () => {
+ const metrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 10,
+ totalSuccess: 5,
+ totalFail: 5,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+ const { lastFrame } = renderWithMockedStats(metrics);
+ expect(lastFrame()).toMatchSnapshot();
+ });
+ });
+
+ describe('Title Rendering', () => {
+ const zeroMetrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ it('renders the default title when no title prop is provided', () => {
+ const { lastFrame } = renderWithMockedStats(zeroMetrics);
+ const output = lastFrame();
+ expect(output).toContain('Session Stats');
+ expect(output).not.toContain('Agent powering down');
+ expect(output).toMatchSnapshot();
+ });
+
+ it('renders the custom title when a title prop is provided', () => {
+ useSessionStatsMock.mockReturnValue({
+ stats: {
+ sessionStartTime: new Date(),
+ metrics: zeroMetrics,
+ lastPromptTokenCount: 0,
+ promptCount: 5,
+ },
+
+ getPromptCount: () => 5,
+ startNewPrompt: vi.fn(),
+ });
+
+ const { lastFrame } = render(
+ ,
+ );
+ const output = lastFrame();
+ expect(output).toContain('Agent powering down. Goodbye!');
+ expect(output).not.toContain('Session Stats');
+ expect(output).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx
new file mode 100644
index 00000000..014026ff
--- /dev/null
+++ b/packages/cli/src/ui/components/StatsDisplay.tsx
@@ -0,0 +1,259 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import Gradient from 'ink-gradient';
+import { Colors } from '../colors.js';
+import { formatDuration } from '../utils/formatters.js';
+import { useSessionStats, ModelMetrics } from '../contexts/SessionContext.js';
+import {
+ getStatusColor,
+ TOOL_SUCCESS_RATE_HIGH,
+ TOOL_SUCCESS_RATE_MEDIUM,
+ USER_AGREEMENT_RATE_HIGH,
+ USER_AGREEMENT_RATE_MEDIUM,
+} from '../utils/displayUtils.js';
+import { computeSessionStats } from '../utils/computeStats.js';
+
+// A more flexible and powerful StatRow component
+interface StatRowProps {
+ title: string;
+ children: React.ReactNode; // Use children to allow for complex, colored values
+}
+
+const StatRow: React.FC = ({ title, children }) => (
+
+ {/* Fixed width for the label creates a clean "gutter" for alignment */}
+
+ {title}
+
+ {children}
+
+);
+
+// A SubStatRow for indented, secondary information
+interface SubStatRowProps {
+ title: string;
+ children: React.ReactNode;
+}
+
+const SubStatRow: React.FC = ({ title, children }) => (
+
+ {/* Adjust width for the "ยป " prefix */}
+
+ ยป {title}
+
+ {children}
+
+);
+
+// A Section component to group related stats
+interface SectionProps {
+ title: string;
+ children: React.ReactNode;
+}
+
+const Section: React.FC = ({ title, children }) => (
+
+ {title}
+ {children}
+
+);
+
+const ModelUsageTable: React.FC<{
+ models: Record;
+ totalCachedTokens: number;
+ cacheEfficiency: number;
+}> = ({ models, totalCachedTokens, cacheEfficiency }) => {
+ const nameWidth = 25;
+ const requestsWidth = 8;
+ const inputTokensWidth = 15;
+ const outputTokensWidth = 15;
+
+ return (
+
+ {/* Header */}
+
+
+ Model Usage
+
+
+ Reqs
+
+
+ Input Tokens
+
+
+ Output Tokens
+
+
+ {/* Divider */}
+
+
+ {/* Rows */}
+ {Object.entries(models).map(([name, modelMetrics]) => (
+
+
+ {name.replace('-001', '')}
+
+
+ {modelMetrics.api.totalRequests}
+
+
+
+ {modelMetrics.tokens.prompt.toLocaleString()}
+
+
+
+
+ {modelMetrics.tokens.candidates.toLocaleString()}
+
+
+
+ ))}
+ {cacheEfficiency > 0 && (
+
+
+ Savings Highlight:{' '}
+ {totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)}
+ %) of input tokens were served from the cache, reducing costs.
+
+
+
+ ยป Tip: For a full token breakdown, run `/stats model`.
+
+
+ )}
+
+ );
+};
+
+interface StatsDisplayProps {
+ duration: string;
+ title?: string;
+}
+
+export const StatsDisplay: React.FC = ({
+ duration,
+ title,
+}) => {
+ const { stats } = useSessionStats();
+ const { metrics } = stats;
+ const { models, tools } = metrics;
+ const computed = computeSessionStats(metrics);
+
+ const successThresholds = {
+ green: TOOL_SUCCESS_RATE_HIGH,
+ yellow: TOOL_SUCCESS_RATE_MEDIUM,
+ };
+ const agreementThresholds = {
+ green: USER_AGREEMENT_RATE_HIGH,
+ yellow: USER_AGREEMENT_RATE_MEDIUM,
+ };
+ const successColor = getStatusColor(computed.successRate, successThresholds);
+ const agreementColor = getStatusColor(
+ computed.agreementRate,
+ agreementThresholds,
+ );
+
+ const renderTitle = () => {
+ if (title) {
+ return Colors.GradientColors && Colors.GradientColors.length > 0 ? (
+
+ {title}
+
+ ) : (
+
+ {title}
+
+ );
+ }
+ return (
+
+ Session Stats
+
+ );
+ };
+
+ return (
+
+ {renderTitle()}
+
+
+ {tools.totalCalls > 0 && (
+
+
+
+ {tools.totalCalls} ({' '}
+ โ {tools.totalSuccess}{' '}
+ โ {tools.totalFail} )
+
+
+
+ {computed.successRate.toFixed(1)}%
+
+ {computed.totalDecisions > 0 && (
+
+
+ {computed.agreementRate.toFixed(1)}%{' '}
+
+ ({computed.totalDecisions} reviewed)
+
+
+
+ )}
+
+ )}
+
+
+
+ {duration}
+
+
+ {formatDuration(computed.agentActiveTime)}
+
+
+
+ {formatDuration(computed.totalApiTime)}{' '}
+
+ ({computed.apiTimePercent.toFixed(1)}%)
+
+
+
+
+
+ {formatDuration(computed.totalToolTime)}{' '}
+
+ ({computed.toolTimePercent.toFixed(1)}%)
+
+
+
+
+
+ {Object.keys(models).length > 0 && (
+
+ )}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.tsx
new file mode 100644
index 00000000..0620665f
--- /dev/null
+++ b/packages/cli/src/ui/components/SuggestionsDisplay.tsx
@@ -0,0 +1,93 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+export interface Suggestion {
+ label: string;
+ value: string;
+ description?: string;
+}
+interface SuggestionsDisplayProps {
+ suggestions: Suggestion[];
+ activeIndex: number;
+ isLoading: boolean;
+ width: number;
+ scrollOffset: number;
+ userInput: string;
+}
+
+export const MAX_SUGGESTIONS_TO_SHOW = 8;
+
+export function SuggestionsDisplay({
+ suggestions,
+ activeIndex,
+ isLoading,
+ width,
+ scrollOffset,
+ userInput,
+}: SuggestionsDisplayProps) {
+ if (isLoading) {
+ return (
+
+ Loading suggestions...
+
+ );
+ }
+
+ if (suggestions.length === 0) {
+ return null; // Don't render anything if there are no suggestions
+ }
+
+ // Calculate the visible slice based on scrollOffset
+ const startIndex = scrollOffset;
+ const endIndex = Math.min(
+ scrollOffset + MAX_SUGGESTIONS_TO_SHOW,
+ suggestions.length,
+ );
+ const visibleSuggestions = suggestions.slice(startIndex, endIndex);
+
+ return (
+
+ {scrollOffset > 0 && โฒ}
+
+ {visibleSuggestions.map((suggestion, index) => {
+ const originalIndex = startIndex + index;
+ const isActive = originalIndex === activeIndex;
+ const textColor = isActive ? Colors.AccentPurple : Colors.Gray;
+
+ return (
+
+
+ {userInput.startsWith('/') ? (
+ // only use box model for (/) command mode
+
+ {suggestion.label}
+
+ ) : (
+ // use regular text for other modes (@ context)
+ {suggestion.label}
+ )}
+ {suggestion.description ? (
+
+
+ {suggestion.description}
+
+
+ ) : null}
+
+
+ );
+ })}
+ {endIndex < suggestions.length && โผ}
+ {suggestions.length > MAX_SUGGESTIONS_TO_SHOW && (
+
+ ({activeIndex + 1}/{suggestions.length})
+
+ )}
+
+ );
+}
diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx
new file mode 100644
index 00000000..0ca176cb
--- /dev/null
+++ b/packages/cli/src/ui/components/ThemeDialog.tsx
@@ -0,0 +1,272 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useCallback, useState } from 'react';
+import { Box, Text, useInput } from 'ink';
+import { Colors } from '../colors.js';
+import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
+import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
+import { DiffRenderer } from './messages/DiffRenderer.js';
+import { colorizeCode } from '../utils/CodeColorizer.js';
+import { LoadedSettings, SettingScope } from '../../config/settings.js';
+
+interface ThemeDialogProps {
+ /** Callback function when a theme is selected */
+ onSelect: (themeName: string | undefined, scope: SettingScope) => void;
+
+ /** Callback function when a theme is highlighted */
+ onHighlight: (themeName: string | undefined) => void;
+ /** The settings object */
+ settings: LoadedSettings;
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+}
+
+export function ThemeDialog({
+ onSelect,
+ onHighlight,
+ settings,
+ availableTerminalHeight,
+ terminalWidth,
+}: ThemeDialogProps): React.JSX.Element {
+ const [selectedScope, setSelectedScope] = useState(
+ SettingScope.User,
+ );
+
+ // Generate theme items
+ const themeItems = themeManager.getAvailableThemes().map((theme) => {
+ const typeString = theme.type.charAt(0).toUpperCase() + theme.type.slice(1);
+ return {
+ label: theme.name,
+ value: theme.name,
+ themeNameDisplay: theme.name,
+ themeTypeDisplay: typeString,
+ };
+ });
+ const [selectInputKey, setSelectInputKey] = useState(Date.now());
+
+ // Determine which radio button should be initially selected in the theme list
+ // This should reflect the theme *saved* for the selected scope, or the default
+ const initialThemeIndex = themeItems.findIndex(
+ (item) => item.value === (settings.merged.theme || DEFAULT_THEME.name),
+ );
+
+ const scopeItems = [
+ { label: 'User Settings', value: SettingScope.User },
+ { label: 'Workspace Settings', value: SettingScope.Workspace },
+ { label: 'System Settings', value: SettingScope.System },
+ ];
+
+ const handleThemeSelect = useCallback(
+ (themeName: string) => {
+ onSelect(themeName, selectedScope);
+ },
+ [onSelect, selectedScope],
+ );
+
+ const handleScopeHighlight = useCallback((scope: SettingScope) => {
+ setSelectedScope(scope);
+ setSelectInputKey(Date.now());
+ }, []);
+
+ const handleScopeSelect = useCallback(
+ (scope: SettingScope) => {
+ handleScopeHighlight(scope);
+ setFocusedSection('theme'); // Reset focus to theme section
+ },
+ [handleScopeHighlight],
+ );
+
+ const [focusedSection, setFocusedSection] = useState<'theme' | 'scope'>(
+ 'theme',
+ );
+
+ useInput((input, key) => {
+ if (key.tab) {
+ setFocusedSection((prev) => (prev === 'theme' ? 'scope' : 'theme'));
+ }
+ if (key.escape) {
+ onSelect(undefined, selectedScope);
+ }
+ });
+
+ const otherScopes = Object.values(SettingScope).filter(
+ (scope) => scope !== selectedScope,
+ );
+
+ const modifiedInOtherScopes = otherScopes.filter(
+ (scope) => settings.forScope(scope).settings.theme !== undefined,
+ );
+
+ let otherScopeModifiedMessage = '';
+ if (modifiedInOtherScopes.length > 0) {
+ const modifiedScopesStr = modifiedInOtherScopes.join(', ');
+ otherScopeModifiedMessage =
+ settings.forScope(selectedScope).settings.theme !== undefined
+ ? `(Also modified in ${modifiedScopesStr})`
+ : `(Modified in ${modifiedScopesStr})`;
+ }
+
+ // Constants for calculating preview pane layout.
+ // These values are based on the JSX structure below.
+ const PREVIEW_PANE_WIDTH_PERCENTAGE = 0.55;
+ // A safety margin to prevent text from touching the border.
+ // This is a complete hack unrelated to the 0.9 used in App.tsx
+ const PREVIEW_PANE_WIDTH_SAFETY_MARGIN = 0.9;
+ // Combined horizontal padding from the dialog and preview pane.
+ const TOTAL_HORIZONTAL_PADDING = 4;
+ const colorizeCodeWidth = Math.max(
+ Math.floor(
+ (terminalWidth - TOTAL_HORIZONTAL_PADDING) *
+ PREVIEW_PANE_WIDTH_PERCENTAGE *
+ PREVIEW_PANE_WIDTH_SAFETY_MARGIN,
+ ),
+ 1,
+ );
+
+ const DIALOG_PADDING = 2;
+ const selectThemeHeight = themeItems.length + 1;
+ const SCOPE_SELECTION_HEIGHT = 4; // Height for the scope selection section + margin.
+ const SPACE_BETWEEN_THEME_SELECTION_AND_APPLY_TO = 1;
+ const TAB_TO_SELECT_HEIGHT = 2;
+ availableTerminalHeight = availableTerminalHeight ?? Number.MAX_SAFE_INTEGER;
+ availableTerminalHeight -= 2; // Top and bottom borders.
+ availableTerminalHeight -= TAB_TO_SELECT_HEIGHT;
+
+ let totalLeftHandSideHeight =
+ DIALOG_PADDING +
+ selectThemeHeight +
+ SCOPE_SELECTION_HEIGHT +
+ SPACE_BETWEEN_THEME_SELECTION_AND_APPLY_TO;
+
+ let showScopeSelection = true;
+ let includePadding = true;
+
+ // Remove content from the LHS that can be omitted if it exceeds the available height.
+ if (totalLeftHandSideHeight > availableTerminalHeight) {
+ includePadding = false;
+ totalLeftHandSideHeight -= DIALOG_PADDING;
+ }
+
+ if (totalLeftHandSideHeight > availableTerminalHeight) {
+ // First, try hiding the scope selection
+ totalLeftHandSideHeight -= SCOPE_SELECTION_HEIGHT;
+ showScopeSelection = false;
+ }
+
+ // Don't focus the scope selection if it is hidden due to height constraints.
+ const currenFocusedSection = !showScopeSelection ? 'theme' : focusedSection;
+
+ // Vertical space taken by elements other than the two code blocks in the preview pane.
+ // Includes "Preview" title, borders, and margin between blocks.
+ const PREVIEW_PANE_FIXED_VERTICAL_SPACE = 8;
+
+ // The right column doesn't need to ever be shorter than the left column.
+ availableTerminalHeight = Math.max(
+ availableTerminalHeight,
+ totalLeftHandSideHeight,
+ );
+ const availableTerminalHeightCodeBlock =
+ availableTerminalHeight -
+ PREVIEW_PANE_FIXED_VERTICAL_SPACE -
+ (includePadding ? 2 : 0) * 2;
+ // Give slightly more space to the code block as it is 3 lines longer.
+ const diffHeight = Math.floor(availableTerminalHeightCodeBlock / 2) - 1;
+ const codeBlockHeight = Math.ceil(availableTerminalHeightCodeBlock / 2) + 1;
+
+ return (
+
+
+ {/* Left Column: Selection */}
+
+
+ {currenFocusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
+ {otherScopeModifiedMessage}
+
+
+
+ {/* Scope Selection */}
+ {showScopeSelection && (
+
+
+ {currenFocusedSection === 'scope' ? '> ' : ' '}Apply To
+
+
+
+ )}
+
+
+ {/* Right Column: Preview */}
+
+ Preview
+
+ {colorizeCode(
+ `# function
+-def fibonacci(n):
+- a, b = 0, 1
+- for _ in range(n):
+- a, b = b, a + b
+- return a`,
+ 'python',
+ codeBlockHeight,
+ colorizeCodeWidth,
+ )}
+
+
+
+
+
+
+
+ (Use Enter to select
+ {showScopeSelection ? ', Tab to change focus' : ''})
+
+
+
+ );
+}
diff --git a/packages/cli/src/ui/components/Tips.tsx b/packages/cli/src/ui/components/Tips.tsx
new file mode 100644
index 00000000..76759050
--- /dev/null
+++ b/packages/cli/src/ui/components/Tips.tsx
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { type Config } from '@qwen/qwen-code-core';
+
+interface TipsProps {
+ config: Config;
+}
+
+export const Tips: React.FC = ({ config }) => {
+ const geminiMdFileCount = config.getGeminiMdFileCount();
+ return (
+
+ Tips for getting started:
+
+ 1. Ask questions, edit files, or run commands.
+
+
+ 2. Be specific for the best results.
+
+ {geminiMdFileCount === 0 && (
+
+ 3. Create{' '}
+
+ QWEN.md
+ {' '}
+ files to customize your interactions with Qwen Code.
+
+ )}
+
+ {geminiMdFileCount === 0 ? '4.' : '3.'}{' '}
+
+ /help
+ {' '}
+ for more information.
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx b/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx
new file mode 100644
index 00000000..e48fcc83
--- /dev/null
+++ b/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx
@@ -0,0 +1,180 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { ToolStatsDisplay } from './ToolStatsDisplay.js';
+import * as SessionContext from '../contexts/SessionContext.js';
+import { SessionMetrics } from '../contexts/SessionContext.js';
+
+// Mock the context to provide controlled data for testing
+vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useSessionStats: vi.fn(),
+ };
+});
+
+const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
+
+const renderWithMockedStats = (metrics: SessionMetrics) => {
+ useSessionStatsMock.mockReturnValue({
+ stats: {
+ sessionStartTime: new Date(),
+ metrics,
+ lastPromptTokenCount: 0,
+ promptCount: 5,
+ },
+
+ getPromptCount: () => 5,
+ startNewPrompt: vi.fn(),
+ });
+
+ return render();
+};
+
+describe('', () => {
+ it('should render "no tool calls" message when there are no active tools', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {},
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ });
+
+ expect(lastFrame()).toContain(
+ 'No tool calls have been made in this session.',
+ );
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('should display stats for a single tool correctly', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {},
+ tools: {
+ totalCalls: 1,
+ totalSuccess: 1,
+ totalFail: 0,
+ totalDurationMs: 100,
+ totalDecisions: { accept: 1, reject: 0, modify: 0 },
+ byName: {
+ 'test-tool': {
+ count: 1,
+ success: 1,
+ fail: 0,
+ durationMs: 100,
+ decisions: { accept: 1, reject: 0, modify: 0 },
+ },
+ },
+ },
+ });
+
+ const output = lastFrame();
+ expect(output).toContain('test-tool');
+ expect(output).toMatchSnapshot();
+ });
+
+ it('should display stats for multiple tools correctly', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {},
+ tools: {
+ totalCalls: 3,
+ totalSuccess: 2,
+ totalFail: 1,
+ totalDurationMs: 300,
+ totalDecisions: { accept: 1, reject: 1, modify: 1 },
+ byName: {
+ 'tool-a': {
+ count: 2,
+ success: 1,
+ fail: 1,
+ durationMs: 200,
+ decisions: { accept: 1, reject: 1, modify: 0 },
+ },
+ 'tool-b': {
+ count: 1,
+ success: 1,
+ fail: 0,
+ durationMs: 100,
+ decisions: { accept: 0, reject: 0, modify: 1 },
+ },
+ },
+ },
+ });
+
+ const output = lastFrame();
+ expect(output).toContain('tool-a');
+ expect(output).toContain('tool-b');
+ expect(output).toMatchSnapshot();
+ });
+
+ it('should handle large values without wrapping or overlapping', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {},
+ tools: {
+ totalCalls: 999999999,
+ totalSuccess: 888888888,
+ totalFail: 111111111,
+ totalDurationMs: 987654321,
+ totalDecisions: {
+ accept: 123456789,
+ reject: 98765432,
+ modify: 12345,
+ },
+ byName: {
+ 'long-named-tool-for-testing-wrapping-and-such': {
+ count: 999999999,
+ success: 888888888,
+ fail: 111111111,
+ durationMs: 987654321,
+ decisions: {
+ accept: 123456789,
+ reject: 98765432,
+ modify: 12345,
+ },
+ },
+ },
+ },
+ });
+
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('should handle zero decisions gracefully', () => {
+ const { lastFrame } = renderWithMockedStats({
+ models: {},
+ tools: {
+ totalCalls: 1,
+ totalSuccess: 1,
+ totalFail: 0,
+ totalDurationMs: 100,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {
+ 'test-tool': {
+ count: 1,
+ success: 1,
+ fail: 0,
+ durationMs: 100,
+ decisions: { accept: 0, reject: 0, modify: 0 },
+ },
+ },
+ },
+ });
+
+ const output = lastFrame();
+ expect(output).toContain('Total Reviewed Suggestions:');
+ expect(output).toContain('0');
+ expect(output).toContain('Overall Agreement Rate:');
+ expect(output).toContain('--');
+ expect(output).toMatchSnapshot();
+ });
+});
diff --git a/packages/cli/src/ui/components/ToolStatsDisplay.tsx b/packages/cli/src/ui/components/ToolStatsDisplay.tsx
new file mode 100644
index 00000000..faa9b6f9
--- /dev/null
+++ b/packages/cli/src/ui/components/ToolStatsDisplay.tsx
@@ -0,0 +1,208 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { formatDuration } from '../utils/formatters.js';
+import {
+ getStatusColor,
+ TOOL_SUCCESS_RATE_HIGH,
+ TOOL_SUCCESS_RATE_MEDIUM,
+ USER_AGREEMENT_RATE_HIGH,
+ USER_AGREEMENT_RATE_MEDIUM,
+} from '../utils/displayUtils.js';
+import { useSessionStats } from '../contexts/SessionContext.js';
+import { ToolCallStats } from '@qwen/qwen-code-core';
+
+const TOOL_NAME_COL_WIDTH = 25;
+const CALLS_COL_WIDTH = 8;
+const SUCCESS_RATE_COL_WIDTH = 15;
+const AVG_DURATION_COL_WIDTH = 15;
+
+const StatRow: React.FC<{
+ name: string;
+ stats: ToolCallStats;
+}> = ({ name, stats }) => {
+ const successRate = stats.count > 0 ? (stats.success / stats.count) * 100 : 0;
+ const avgDuration = stats.count > 0 ? stats.durationMs / stats.count : 0;
+ const successColor = getStatusColor(successRate, {
+ green: TOOL_SUCCESS_RATE_HIGH,
+ yellow: TOOL_SUCCESS_RATE_MEDIUM,
+ });
+
+ return (
+
+
+ {name}
+
+
+ {stats.count}
+
+
+ {successRate.toFixed(1)}%
+
+
+ {formatDuration(avgDuration)}
+
+
+ );
+};
+
+export const ToolStatsDisplay: React.FC = () => {
+ const { stats } = useSessionStats();
+ const { tools } = stats.metrics;
+ const activeTools = Object.entries(tools.byName).filter(
+ ([, metrics]) => metrics.count > 0,
+ );
+
+ if (activeTools.length === 0) {
+ return (
+
+ No tool calls have been made in this session.
+
+ );
+ }
+
+ const totalDecisions = Object.values(tools.byName).reduce(
+ (acc, tool) => {
+ acc.accept += tool.decisions.accept;
+ acc.reject += tool.decisions.reject;
+ acc.modify += tool.decisions.modify;
+ return acc;
+ },
+ { accept: 0, reject: 0, modify: 0 },
+ );
+
+ const totalReviewed =
+ totalDecisions.accept + totalDecisions.reject + totalDecisions.modify;
+ const agreementRate =
+ totalReviewed > 0 ? (totalDecisions.accept / totalReviewed) * 100 : 0;
+ const agreementColor = getStatusColor(agreementRate, {
+ green: USER_AGREEMENT_RATE_HIGH,
+ yellow: USER_AGREEMENT_RATE_MEDIUM,
+ });
+
+ return (
+
+
+ Tool Stats For Nerds
+
+
+
+ {/* Header */}
+
+
+ Tool Name
+
+
+ Calls
+
+
+ Success Rate
+
+
+ Avg Duration
+
+
+
+ {/* Divider */}
+
+
+ {/* Tool Rows */}
+ {activeTools.map(([name, stats]) => (
+
+ ))}
+
+
+
+ {/* User Decision Summary */}
+ User Decision Summary
+
+
+ Total Reviewed Suggestions:
+
+
+ {totalReviewed}
+
+
+
+
+ ยป Accepted:
+
+
+ {totalDecisions.accept}
+
+
+
+
+ ยป Rejected:
+
+
+ {totalDecisions.reject}
+
+
+
+
+ ยป Modified:
+
+
+ {totalDecisions.modify}
+
+
+
+ {/* Divider */}
+
+
+
+
+ Overall Agreement Rate:
+
+
+ 0 ? agreementColor : undefined}>
+ {totalReviewed > 0 ? `${agreementRate.toFixed(1)}%` : '--'}
+
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/UpdateNotification.tsx b/packages/cli/src/ui/components/UpdateNotification.tsx
new file mode 100644
index 00000000..b88c9bd5
--- /dev/null
+++ b/packages/cli/src/ui/components/UpdateNotification.tsx
@@ -0,0 +1,23 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+
+interface UpdateNotificationProps {
+ message: string;
+}
+
+export const UpdateNotification = ({ message }: UpdateNotificationProps) => (
+
+ {message}
+
+);
diff --git a/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap
new file mode 100644
index 00000000..efc0862b
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap
@@ -0,0 +1,121 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` > should display a single model correctly 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Model Stats For Nerds โ
+โ โ
+โ Metric gemini-2.5-pro โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ API โ
+โ Requests 1 โ
+โ Errors 0 (0.0%) โ
+โ Avg Latency 100ms โ
+โ โ
+โ Tokens โ
+โ Total 30 โ
+โ โณ Prompt 10 โ
+โ โณ Cached 5 (50.0%) โ
+โ โณ Thoughts 2 โ
+โ โณ Tool 1 โ
+โ โณ Output 20 โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should display conditional rows if at least one model has data 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Model Stats For Nerds โ
+โ โ
+โ Metric gemini-2.5-pro gemini-2.5-flash โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ API โ
+โ Requests 1 1 โ
+โ Errors 0 (0.0%) 0 (0.0%) โ
+โ Avg Latency 100ms 50ms โ
+โ โ
+โ Tokens โ
+โ Total 30 15 โ
+โ โณ Prompt 10 5 โ
+โ โณ Cached 5 (50.0%) 0 (0.0%) โ
+โ โณ Thoughts 2 0 โ
+โ โณ Tool 0 3 โ
+โ โณ Output 20 10 โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should display stats for multiple models correctly 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Model Stats For Nerds โ
+โ โ
+โ Metric gemini-2.5-pro gemini-2.5-flash โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ API โ
+โ Requests 10 20 โ
+โ Errors 1 (10.0%) 2 (10.0%) โ
+โ Avg Latency 100ms 25ms โ
+โ โ
+โ Tokens โ
+โ Total 300 600 โ
+โ โณ Prompt 100 200 โ
+โ โณ Cached 50 (50.0%) 100 (50.0%) โ
+โ โณ Thoughts 10 20 โ
+โ โณ Tool 5 10 โ
+โ โณ Output 200 400 โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should handle large values without wrapping or overlapping 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Model Stats For Nerds โ
+โ โ
+โ Metric gemini-2.5-pro โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ API โ
+โ Requests 999,999,999 โ
+โ Errors 123,456,789 (12.3%) โ
+โ Avg Latency 0ms โ
+โ โ
+โ Tokens โ
+โ Total 999,999,999 โ
+โ โณ Prompt 987,654,321 โ
+โ โณ Cached 123,456,789 (12.5%) โ
+โ โณ Thoughts 111,111,111 โ
+โ โณ Tool 222,222,222 โ
+โ โณ Output 123,456,789 โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should not display conditional rows if no model has data for them 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Model Stats For Nerds โ
+โ โ
+โ Metric gemini-2.5-pro โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ API โ
+โ Requests 1 โ
+โ Errors 0 (0.0%) โ
+โ Avg Latency 100ms โ
+โ โ
+โ Tokens โ
+โ Total 30 โ
+โ โณ Prompt 10 โ
+โ โณ Output 20 โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should render "no API calls" message when there are no active models 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ No API calls have been made in this session. โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
diff --git a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap
new file mode 100644
index 00000000..c9b2bd64
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap
@@ -0,0 +1,24 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` > renders the summary display with a title 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Agent powering down. Goodbye! โ
+โ โ
+โ Performance โ
+โ Wall Time: 1h 23m 45s โ
+โ Agent Active: 50.2s โ
+โ ยป API Time: 50.2s (100.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โ Model Usage Reqs Input Tokens Output Tokens โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ gemini-2.5-pro 10 1,000 2,000 โ
+โ โ
+โ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. โ
+โ โ
+โ ยป Tip: For a full token breakdown, run \`/stats model\`. โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
diff --git a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
new file mode 100644
index 00000000..c7c2ec59
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
@@ -0,0 +1,193 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` > Conditional Color Tests > renders success rate in green for high values 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Interaction Summary โ
+โ Tool Calls: 10 ( โ 10 โ 0 ) โ
+โ Success Rate: 100.0% โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 0s โ
+โ ยป API Time: 0s (0.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > Conditional Color Tests > renders success rate in red for low values 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Interaction Summary โ
+โ Tool Calls: 10 ( โ 5 โ 5 ) โ
+โ Success Rate: 50.0% โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 0s โ
+โ ยป API Time: 0s (0.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > Conditional Color Tests > renders success rate in yellow for medium values 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Interaction Summary โ
+โ Tool Calls: 10 ( โ 9 โ 1 ) โ
+โ Success Rate: 90.0% โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 0s โ
+โ ยป API Time: 0s (0.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > Conditional Rendering Tests > hides Efficiency section when cache is not used 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 100ms โ
+โ ยป API Time: 100ms (100.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โ Model Usage Reqs Input Tokens Output Tokens โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ gemini-2.5-pro 1 100 100 โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > Conditional Rendering Tests > hides User Agreement when no decisions are made 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Interaction Summary โ
+โ Tool Calls: 2 ( โ 1 โ 1 ) โ
+โ Success Rate: 50.0% โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 123ms โ
+โ ยป API Time: 0s (0.0%) โ
+โ ยป Tool Time: 123ms (100.0%) โ
+โ โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > Title Rendering > renders the custom title when a title prop is provided 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Agent powering down. Goodbye! โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 0s โ
+โ ยป API Time: 0s (0.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > Title Rendering > renders the default title when no title prop is provided 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 0s โ
+โ ยป API Time: 0s (0.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > renders a table with two models correctly 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 19.5s โ
+โ ยป API Time: 19.5s (100.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โ Model Usage Reqs Input Tokens Output Tokens โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ gemini-2.5-pro 3 1,000 2,000 โ
+โ gemini-2.5-flash 5 25,000 15,000 โ
+โ โ
+โ Savings Highlight: 10,500 (40.4%) of input tokens were served from the cache, reducing costs. โ
+โ โ
+โ ยป Tip: For a full token breakdown, run \`/stats model\`. โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > renders all sections when all data is present 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Interaction Summary โ
+โ Tool Calls: 2 ( โ 1 โ 1 ) โ
+โ Success Rate: 50.0% โ
+โ User Agreement: 100.0% (1 reviewed) โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 223ms โ
+โ ยป API Time: 100ms (44.8%) โ
+โ ยป Tool Time: 123ms (55.2%) โ
+โ โ
+โ โ
+โ Model Usage Reqs Input Tokens Output Tokens โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ gemini-2.5-pro 1 100 100 โ
+โ โ
+โ Savings Highlight: 50 (50.0%) of input tokens were served from the cache, reducing costs. โ
+โ โ
+โ ยป Tip: For a full token breakdown, run \`/stats model\`. โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > renders only the Performance section in its zero state 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Session Stats โ
+โ โ
+โ Performance โ
+โ Wall Time: 1s โ
+โ Agent Active: 0s โ
+โ ยป API Time: 0s (0.0%) โ
+โ ยป Tool Time: 0s (0.0%) โ
+โ โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
diff --git a/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap
new file mode 100644
index 00000000..61fb3efc
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap
@@ -0,0 +1,91 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` > should display stats for a single tool correctly 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Tool Stats For Nerds โ
+โ โ
+โ Tool Name Calls Success Rate Avg Duration โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ test-tool 1 100.0% 100ms โ
+โ โ
+โ User Decision Summary โ
+โ Total Reviewed Suggestions: 1 โ
+โ ยป Accepted: 1 โ
+โ ยป Rejected: 0 โ
+โ ยป Modified: 0 โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ Overall Agreement Rate: 100.0% โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should display stats for multiple tools correctly 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Tool Stats For Nerds โ
+โ โ
+โ Tool Name Calls Success Rate Avg Duration โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ tool-a 2 50.0% 100ms โ
+โ tool-b 1 100.0% 100ms โ
+โ โ
+โ User Decision Summary โ
+โ Total Reviewed Suggestions: 3 โ
+โ ยป Accepted: 1 โ
+โ ยป Rejected: 1 โ
+โ ยป Modified: 1 โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ Overall Agreement Rate: 33.3% โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should handle large values without wrapping or overlapping 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Tool Stats For Nerds โ
+โ โ
+โ Tool Name Calls Success Rate Avg Duration โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ long-named-tool-for-testi99999999 88.9% 1ms โ
+โ ng-wrapping-and-such 9 โ
+โ โ
+โ User Decision Summary โ
+โ Total Reviewed Suggestions: 222234566 โ
+โ ยป Accepted: 123456789 โ
+โ ยป Rejected: 98765432 โ
+โ ยป Modified: 12345 โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ Overall Agreement Rate: 55.6% โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should handle zero decisions gracefully 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ Tool Stats For Nerds โ
+โ โ
+โ Tool Name Calls Success Rate Avg Duration โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ test-tool 1 100.0% 100ms โ
+โ โ
+โ User Decision Summary โ
+โ Total Reviewed Suggestions: 0 โ
+โ ยป Accepted: 0 โ
+โ ยป Rejected: 0 โ
+โ ยป Modified: 0 โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ Overall Agreement Rate: -- โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
+
+exports[` > should render "no tool calls" message when there are no active tools 1`] = `
+"โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โ
+โ No tool calls have been made in this session. โ
+โ โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ"
+`;
diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.tsx
new file mode 100644
index 00000000..c7ef122b
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/CompressionMessage.tsx
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { CompressionProps } from '../../types.js';
+import Spinner from 'ink-spinner';
+import { Colors } from '../../colors.js';
+
+export interface CompressionDisplayProps {
+ compression: CompressionProps;
+}
+
+/*
+ * Compression messages appear when the /compress command is run, and show a loading spinner
+ * while compression is in progress, followed up by some compression stats.
+ */
+export const CompressionMessage: React.FC = ({
+ compression,
+}) => {
+ const text = compression.isPending
+ ? 'Compressing chat history'
+ : `Chat history compressed from ${compression.originalTokenCount ?? 'unknown'}` +
+ ` to ${compression.newTokenCount ?? 'unknown'} tokens.`;
+
+ return (
+
+
+ {compression.isPending ? (
+
+ ) : (
+ โฆ
+ )}
+
+
+
+ {text}
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
new file mode 100644
index 00000000..a6f906a6
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
@@ -0,0 +1,362 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { OverflowProvider } from '../../contexts/OverflowContext.js';
+import { render } from 'ink-testing-library';
+import { DiffRenderer } from './DiffRenderer.js';
+import * as CodeColorizer from '../../utils/CodeColorizer.js';
+import { vi } from 'vitest';
+
+describe('', () => {
+ const mockColorizeCode = vi.spyOn(CodeColorizer, 'colorizeCode');
+
+ beforeEach(() => {
+ mockColorizeCode.mockClear();
+ });
+
+ const sanitizeOutput = (output: string | undefined, terminalWidth: number) =>
+ output?.replace(/GAP_INDICATOR/g, 'โ'.repeat(terminalWidth));
+
+ it('should call colorizeCode with correct language for new file with known extension', () => {
+ const newFileDiffContent = `
+diff --git a/test.py b/test.py
+new file mode 100644
+index 0000000..e69de29
+--- /dev/null
++++ b/test.py
+@@ -0,0 +1 @@
++print("hello world")
+`;
+ render(
+
+
+ ,
+ );
+ expect(mockColorizeCode).toHaveBeenCalledWith(
+ 'print("hello world")',
+ 'python',
+ undefined,
+ 80,
+ );
+ });
+
+ it('should call colorizeCode with null language for new file with unknown extension', () => {
+ const newFileDiffContent = `
+diff --git a/test.unknown b/test.unknown
+new file mode 100644
+index 0000000..e69de29
+--- /dev/null
++++ b/test.unknown
+@@ -0,0 +1 @@
++some content
+`;
+ render(
+
+
+ ,
+ );
+ expect(mockColorizeCode).toHaveBeenCalledWith(
+ 'some content',
+ null,
+ undefined,
+ 80,
+ );
+ });
+
+ it('should call colorizeCode with null language for new file if no filename is provided', () => {
+ const newFileDiffContent = `
+diff --git a/test.txt b/test.txt
+new file mode 100644
+index 0000000..e69de29
+--- /dev/null
++++ b/test.txt
+@@ -0,0 +1 @@
++some text content
+`;
+ render(
+
+
+ ,
+ );
+ expect(mockColorizeCode).toHaveBeenCalledWith(
+ 'some text content',
+ null,
+ undefined,
+ 80,
+ );
+ });
+
+ it('should render diff content for existing file (not calling colorizeCode directly for the whole block)', () => {
+ const existingFileDiffContent = `
+diff --git a/test.txt b/test.txt
+index 0000001..0000002 100644
+--- a/test.txt
++++ b/test.txt
+@@ -1 +1 @@
+-old line
++new line
+`;
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ // colorizeCode is used internally by the line-by-line rendering, not for the whole block
+ expect(mockColorizeCode).not.toHaveBeenCalledWith(
+ expect.stringContaining('old line'),
+ expect.anything(),
+ );
+ expect(mockColorizeCode).not.toHaveBeenCalledWith(
+ expect.stringContaining('new line'),
+ expect.anything(),
+ );
+ const output = lastFrame();
+ const lines = output!.split('\n');
+ expect(lines[0]).toBe('1 - old line');
+ expect(lines[1]).toBe('1 + new line');
+ });
+
+ it('should handle diff with only header and no changes', () => {
+ const noChangeDiff = `diff --git a/file.txt b/file.txt
+index 1234567..1234567 100644
+--- a/file.txt
++++ b/file.txt
+`;
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ expect(lastFrame()).toContain('No changes detected');
+ expect(mockColorizeCode).not.toHaveBeenCalled();
+ });
+
+ it('should handle empty diff content', () => {
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ expect(lastFrame()).toContain('No diff content');
+ expect(mockColorizeCode).not.toHaveBeenCalled();
+ });
+
+ it('should render a gap indicator for skipped lines', () => {
+ const diffWithGap = `
+diff --git a/file.txt b/file.txt
+index 123..456 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1,2 +1,2 @@
+ context line 1
+-deleted line
++added line
+@@ -10,2 +10,2 @@
+ context line 10
+ context line 11
+`;
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ const output = lastFrame();
+ expect(output).toContain('โ'); // Check for the border character used in the gap
+
+ // Verify that lines before and after the gap are rendered
+ expect(output).toContain('context line 1');
+ expect(output).toContain('added line');
+ expect(output).toContain('context line 10');
+ });
+
+ it('should not render a gap indicator for small gaps (<= MAX_CONTEXT_LINES_WITHOUT_GAP)', () => {
+ const diffWithSmallGap = `
+diff --git a/file.txt b/file.txt
+index abc..def 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1,5 +1,5 @@
+ context line 1
+ context line 2
+ context line 3
+ context line 4
+ context line 5
+@@ -11,5 +11,5 @@
+ context line 11
+ context line 12
+ context line 13
+ context line 14
+ context line 15
+`;
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ const output = lastFrame();
+ expect(output).not.toContain('โ'); // Ensure no separator is rendered
+
+ // Verify that lines before and after the gap are rendered
+ expect(output).toContain('context line 5');
+ expect(output).toContain('context line 11');
+ });
+
+ describe('should correctly render a diff with multiple hunks and a gap indicator', () => {
+ const diffWithMultipleHunks = `
+diff --git a/multi.js b/multi.js
+index 123..789 100644
+--- a/multi.js
++++ b/multi.js
+@@ -1,3 +1,3 @@
+ console.log('first hunk');
+-const oldVar = 1;
++const newVar = 1;
+ console.log('end of first hunk');
+@@ -20,3 +20,3 @@
+ console.log('second hunk');
+-const anotherOld = 'test';
++const anotherNew = 'test';
+ console.log('end of second hunk');
+`;
+
+ it.each([
+ {
+ terminalWidth: 80,
+ height: undefined,
+ expected: `1 console.log('first hunk');
+2 - const oldVar = 1;
+2 + const newVar = 1;
+3 console.log('end of first hunk');
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+20 console.log('second hunk');
+21 - const anotherOld = 'test';
+21 + const anotherNew = 'test';
+22 console.log('end of second hunk');`,
+ },
+ {
+ terminalWidth: 80,
+ height: 6,
+ expected: `... first 4 lines hidden ...
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+20 console.log('second hunk');
+21 - const anotherOld = 'test';
+21 + const anotherNew = 'test';
+22 console.log('end of second hunk');`,
+ },
+ {
+ terminalWidth: 30,
+ height: 6,
+ expected: `... first 10 lines hidden ...
+ 'test';
+21 + const anotherNew =
+ 'test';
+22 console.log('end of
+ second hunk');`,
+ },
+ ])(
+ 'with terminalWidth $terminalWidth and height $height',
+ ({ terminalWidth, height, expected }) => {
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ const output = lastFrame();
+ expect(sanitizeOutput(output, terminalWidth)).toEqual(expected);
+ },
+ );
+ });
+
+ it('should correctly render a diff with a SVN diff format', () => {
+ const newFileDiff = `
+fileDiff Index: file.txt
+===================================================================
+--- a/file.txt Current
++++ b/file.txt Proposed
+--- a/multi.js
++++ b/multi.js
+@@ -1,1 +1,1 @@
+-const oldVar = 1;
++const newVar = 1;
+@@ -20,1 +20,1 @@
+-const anotherOld = 'test';
++const anotherNew = 'test';
+\\ No newline at end of file
+`;
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ const output = lastFrame();
+
+ expect(output).toEqual(`1 - const oldVar = 1;
+1 + const newVar = 1;
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+20 - const anotherOld = 'test';
+20 + const anotherNew = 'test';`);
+ });
+
+ it('should correctly render a new file with no file extension correctly', () => {
+ const newFileDiff = `
+fileDiff Index: Dockerfile
+===================================================================
+--- Dockerfile Current
++++ Dockerfile Proposed
+@@ -0,0 +1,3 @@
++FROM node:14
++RUN npm install
++RUN npm run build
+\\ No newline at end of file
+`;
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ const output = lastFrame();
+ expect(output).toEqual(`1 FROM node:14
+2 RUN npm install
+3 RUN npm run build`);
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx
new file mode 100644
index 00000000..25fb293e
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx
@@ -0,0 +1,312 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../../colors.js';
+import crypto from 'crypto';
+import { colorizeCode } from '../../utils/CodeColorizer.js';
+import { MaxSizedBox } from '../shared/MaxSizedBox.js';
+
+interface DiffLine {
+ type: 'add' | 'del' | 'context' | 'hunk' | 'other';
+ oldLine?: number;
+ newLine?: number;
+ content: string;
+}
+
+function parseDiffWithLineNumbers(diffContent: string): DiffLine[] {
+ const lines = diffContent.split('\n');
+ const result: DiffLine[] = [];
+ let currentOldLine = 0;
+ let currentNewLine = 0;
+ let inHunk = false;
+ const hunkHeaderRegex = /^@@ -(\d+),?\d* \+(\d+),?\d* @@/;
+
+ for (const line of lines) {
+ const hunkMatch = line.match(hunkHeaderRegex);
+ if (hunkMatch) {
+ currentOldLine = parseInt(hunkMatch[1], 10);
+ currentNewLine = parseInt(hunkMatch[2], 10);
+ inHunk = true;
+ result.push({ type: 'hunk', content: line });
+ // We need to adjust the starting point because the first line number applies to the *first* actual line change/context,
+ // but we increment *before* pushing that line. So decrement here.
+ currentOldLine--;
+ currentNewLine--;
+ continue;
+ }
+ if (!inHunk) {
+ // Skip standard Git header lines more robustly
+ if (
+ line.startsWith('--- ') ||
+ line.startsWith('+++ ') ||
+ line.startsWith('diff --git') ||
+ line.startsWith('index ') ||
+ line.startsWith('similarity index') ||
+ line.startsWith('rename from') ||
+ line.startsWith('rename to') ||
+ line.startsWith('new file mode') ||
+ line.startsWith('deleted file mode')
+ )
+ continue;
+ // If it's not a hunk or header, skip (or handle as 'other' if needed)
+ continue;
+ }
+ if (line.startsWith('+')) {
+ currentNewLine++; // Increment before pushing
+ result.push({
+ type: 'add',
+ newLine: currentNewLine,
+ content: line.substring(1),
+ });
+ } else if (line.startsWith('-')) {
+ currentOldLine++; // Increment before pushing
+ result.push({
+ type: 'del',
+ oldLine: currentOldLine,
+ content: line.substring(1),
+ });
+ } else if (line.startsWith(' ')) {
+ currentOldLine++; // Increment before pushing
+ currentNewLine++;
+ result.push({
+ type: 'context',
+ oldLine: currentOldLine,
+ newLine: currentNewLine,
+ content: line.substring(1),
+ });
+ } else if (line.startsWith('\\')) {
+ // Handle "\ No newline at end of file"
+ result.push({ type: 'other', content: line });
+ }
+ }
+ return result;
+}
+
+interface DiffRendererProps {
+ diffContent: string;
+ filename?: string;
+ tabWidth?: number;
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+}
+
+const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
+
+export const DiffRenderer: React.FC = ({
+ diffContent,
+ filename,
+ tabWidth = DEFAULT_TAB_WIDTH,
+ availableTerminalHeight,
+ terminalWidth,
+}) => {
+ if (!diffContent || typeof diffContent !== 'string') {
+ return No diff content.;
+ }
+
+ const parsedLines = parseDiffWithLineNumbers(diffContent);
+
+ if (parsedLines.length === 0) {
+ return (
+
+ No changes detected.
+
+ );
+ }
+
+ // Check if the diff represents a new file (only additions and header lines)
+ const isNewFile = parsedLines.every(
+ (line) =>
+ line.type === 'add' ||
+ line.type === 'hunk' ||
+ line.type === 'other' ||
+ line.content.startsWith('diff --git') ||
+ line.content.startsWith('new file mode'),
+ );
+
+ let renderedOutput;
+
+ if (isNewFile) {
+ // Extract only the added lines' content
+ const addedContent = parsedLines
+ .filter((line) => line.type === 'add')
+ .map((line) => line.content)
+ .join('\n');
+ // Attempt to infer language from filename, default to plain text if no filename
+ const fileExtension = filename?.split('.').pop() || null;
+ const language = fileExtension
+ ? getLanguageFromExtension(fileExtension)
+ : null;
+ renderedOutput = colorizeCode(
+ addedContent,
+ language,
+ availableTerminalHeight,
+ terminalWidth,
+ );
+ } else {
+ renderedOutput = renderDiffContent(
+ parsedLines,
+ filename,
+ tabWidth,
+ availableTerminalHeight,
+ terminalWidth,
+ );
+ }
+
+ return renderedOutput;
+};
+
+const renderDiffContent = (
+ parsedLines: DiffLine[],
+ filename: string | undefined,
+ tabWidth = DEFAULT_TAB_WIDTH,
+ availableTerminalHeight: number | undefined,
+ terminalWidth: number,
+) => {
+ // 1. Normalize whitespace (replace tabs with spaces) *before* further processing
+ const normalizedLines = parsedLines.map((line) => ({
+ ...line,
+ content: line.content.replace(/\t/g, ' '.repeat(tabWidth)),
+ }));
+
+ // Filter out non-displayable lines (hunks, potentially 'other') using the normalized list
+ const displayableLines = normalizedLines.filter(
+ (l) => l.type !== 'hunk' && l.type !== 'other',
+ );
+
+ if (displayableLines.length === 0) {
+ return (
+
+ No changes detected.
+
+ );
+ }
+
+ // Calculate the minimum indentation across all displayable lines
+ let baseIndentation = Infinity; // Start high to find the minimum
+ for (const line of displayableLines) {
+ // Only consider lines with actual content for indentation calculation
+ if (line.content.trim() === '') continue;
+
+ const firstCharIndex = line.content.search(/\S/); // Find index of first non-whitespace char
+ const currentIndent = firstCharIndex === -1 ? 0 : firstCharIndex; // Indent is 0 if no non-whitespace found
+ baseIndentation = Math.min(baseIndentation, currentIndent);
+ }
+ // If baseIndentation remained Infinity (e.g., no displayable lines with content), default to 0
+ if (!isFinite(baseIndentation)) {
+ baseIndentation = 0;
+ }
+
+ const key = filename
+ ? `diff-box-${filename}`
+ : `diff-box-${crypto.createHash('sha1').update(JSON.stringify(parsedLines)).digest('hex')}`;
+
+ let lastLineNumber: number | null = null;
+ const MAX_CONTEXT_LINES_WITHOUT_GAP = 5;
+
+ return (
+
+ {displayableLines.reduce((acc, line, index) => {
+ // Determine the relevant line number for gap calculation based on type
+ let relevantLineNumberForGapCalc: number | null = null;
+ if (line.type === 'add' || line.type === 'context') {
+ relevantLineNumberForGapCalc = line.newLine ?? null;
+ } else if (line.type === 'del') {
+ // For deletions, the gap is typically in relation to the original file's line numbering
+ relevantLineNumberForGapCalc = line.oldLine ?? null;
+ }
+
+ if (
+ lastLineNumber !== null &&
+ relevantLineNumberForGapCalc !== null &&
+ relevantLineNumberForGapCalc >
+ lastLineNumber + MAX_CONTEXT_LINES_WITHOUT_GAP + 1
+ ) {
+ acc.push(
+
+ {'โ'.repeat(terminalWidth)}
+ ,
+ );
+ }
+
+ const lineKey = `diff-line-${index}`;
+ let gutterNumStr = '';
+ let color: string | undefined = undefined;
+ let prefixSymbol = ' ';
+ let dim = false;
+
+ switch (line.type) {
+ case 'add':
+ gutterNumStr = (line.newLine ?? '').toString();
+ color = 'green';
+ prefixSymbol = '+';
+ lastLineNumber = line.newLine ?? null;
+ break;
+ case 'del':
+ gutterNumStr = (line.oldLine ?? '').toString();
+ color = 'red';
+ prefixSymbol = '-';
+ // For deletions, update lastLineNumber based on oldLine if it's advancing.
+ // This helps manage gaps correctly if there are multiple consecutive deletions
+ // or if a deletion is followed by a context line far away in the original file.
+ if (line.oldLine !== undefined) {
+ lastLineNumber = line.oldLine;
+ }
+ break;
+ case 'context':
+ gutterNumStr = (line.newLine ?? '').toString();
+ dim = true;
+ prefixSymbol = ' ';
+ lastLineNumber = line.newLine ?? null;
+ break;
+ default:
+ return acc;
+ }
+
+ const displayContent = line.content.substring(baseIndentation);
+
+ acc.push(
+
+ {gutterNumStr.padEnd(4)}
+
+ {prefixSymbol}{' '}
+
+
+ {displayContent}
+
+ ,
+ );
+ return acc;
+ }, [])}
+
+ );
+};
+
+const getLanguageFromExtension = (extension: string): string | null => {
+ const languageMap: { [key: string]: string } = {
+ js: 'javascript',
+ ts: 'typescript',
+ py: 'python',
+ json: 'json',
+ css: 'css',
+ html: 'html',
+ sh: 'bash',
+ md: 'markdown',
+ yaml: 'yaml',
+ yml: 'yaml',
+ txt: 'plaintext',
+ java: 'java',
+ c: 'c',
+ cpp: 'cpp',
+ rb: 'ruby',
+ };
+ return languageMap[extension] || null; // Return null if extension not found
+};
diff --git a/packages/cli/src/ui/components/messages/ErrorMessage.tsx b/packages/cli/src/ui/components/messages/ErrorMessage.tsx
new file mode 100644
index 00000000..edbea435
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ErrorMessage.tsx
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Text, Box } from 'ink';
+import { Colors } from '../../colors.js';
+
+interface ErrorMessageProps {
+ text: string;
+}
+
+export const ErrorMessage: React.FC = ({ text }) => {
+ const prefix = 'โ ';
+ const prefixWidth = prefix.length;
+
+ return (
+
+
+ {prefix}
+
+
+
+ {text}
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/GeminiMessage.tsx b/packages/cli/src/ui/components/messages/GeminiMessage.tsx
new file mode 100644
index 00000000..9863acd6
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/GeminiMessage.tsx
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Text, Box } from 'ink';
+import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
+import { Colors } from '../../colors.js';
+
+interface GeminiMessageProps {
+ text: string;
+ isPending: boolean;
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+}
+
+export const GeminiMessage: React.FC = ({
+ text,
+ isPending,
+ availableTerminalHeight,
+ terminalWidth,
+}) => {
+ const prefix = 'โฆ ';
+ const prefixWidth = prefix.length;
+
+ return (
+
+
+ {prefix}
+
+
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx b/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx
new file mode 100644
index 00000000..b5f01599
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box } from 'ink';
+import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
+
+interface GeminiMessageContentProps {
+ text: string;
+ isPending: boolean;
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+}
+
+/*
+ * Gemini message content is a semi-hacked component. The intention is to represent a partial
+ * of GeminiMessage and is only used when a response gets too long. In that instance messages
+ * are split into multiple GeminiMessageContent's to enable the root component in
+ * App.tsx to be as performant as humanly possible.
+ */
+export const GeminiMessageContent: React.FC = ({
+ text,
+ isPending,
+ availableTerminalHeight,
+ terminalWidth,
+}) => {
+ const originalPrefix = 'โฆ ';
+ const prefixWidth = originalPrefix.length;
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/InfoMessage.tsx b/packages/cli/src/ui/components/messages/InfoMessage.tsx
new file mode 100644
index 00000000..c9129999
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/InfoMessage.tsx
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Text, Box } from 'ink';
+import { Colors } from '../../colors.js';
+
+interface InfoMessageProps {
+ text: string;
+}
+
+export const InfoMessage: React.FC = ({ text }) => {
+ const prefix = 'โน ';
+ const prefixWidth = prefix.length;
+
+ return (
+
+
+ {prefix}
+
+
+
+ {text}
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
new file mode 100644
index 00000000..b3704692
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
+import { ToolCallConfirmationDetails } from '@qwen/qwen-code-core';
+
+describe('ToolConfirmationMessage', () => {
+ it('should not display urls if prompt and url are the same', () => {
+ const confirmationDetails: ToolCallConfirmationDetails = {
+ type: 'info',
+ title: 'Confirm Web Fetch',
+ prompt: 'https://example.com',
+ urls: ['https://example.com'],
+ onConfirm: vi.fn(),
+ };
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).not.toContain('URLs to fetch:');
+ });
+
+ it('should display urls if prompt and url are different', () => {
+ const confirmationDetails: ToolCallConfirmationDetails = {
+ type: 'info',
+ title: 'Confirm Web Fetch',
+ prompt:
+ 'fetch https://github.com/google/gemini-react/blob/main/README.md',
+ urls: [
+ 'https://raw.githubusercontent.com/google/gemini-react/main/README.md',
+ ],
+ onConfirm: vi.fn(),
+ };
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('URLs to fetch:');
+ expect(lastFrame()).toContain(
+ '- https://raw.githubusercontent.com/google/gemini-react/main/README.md',
+ );
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
new file mode 100644
index 00000000..8398f7ca
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -0,0 +1,250 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text, useInput } from 'ink';
+import { DiffRenderer } from './DiffRenderer.js';
+import { Colors } from '../../colors.js';
+import {
+ ToolCallConfirmationDetails,
+ ToolConfirmationOutcome,
+ ToolExecuteConfirmationDetails,
+ ToolMcpConfirmationDetails,
+ Config,
+} from '@qwen/qwen-code-core';
+import {
+ RadioButtonSelect,
+ RadioSelectItem,
+} from '../shared/RadioButtonSelect.js';
+import { MaxSizedBox } from '../shared/MaxSizedBox.js';
+
+export interface ToolConfirmationMessageProps {
+ confirmationDetails: ToolCallConfirmationDetails;
+ config?: Config;
+ isFocused?: boolean;
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+}
+
+export const ToolConfirmationMessage: React.FC<
+ ToolConfirmationMessageProps
+> = ({
+ confirmationDetails,
+ isFocused = true,
+ availableTerminalHeight,
+ terminalWidth,
+}) => {
+ const { onConfirm } = confirmationDetails;
+ const childWidth = terminalWidth - 2; // 2 for padding
+
+ useInput((_, key) => {
+ if (!isFocused) return;
+ if (key.escape) {
+ onConfirm(ToolConfirmationOutcome.Cancel);
+ }
+ });
+
+ const handleSelect = (item: ToolConfirmationOutcome) => onConfirm(item);
+
+ let bodyContent: React.ReactNode | null = null; // Removed contextDisplay here
+ let question: string;
+
+ const options: Array> = new Array<
+ RadioSelectItem
+ >();
+
+ // Body content is now the DiffRenderer, passing filename to it
+ // The bordered box is removed from here and handled within DiffRenderer
+
+ function availableBodyContentHeight() {
+ if (options.length === 0) {
+ // This should not happen in practice as options are always added before this is called.
+ throw new Error('Options not provided for confirmation message');
+ }
+
+ if (availableTerminalHeight === undefined) {
+ return undefined;
+ }
+
+ // Calculate the vertical space (in lines) consumed by UI elements
+ // surrounding the main body content.
+ const PADDING_OUTER_Y = 2; // Main container has `padding={1}` (top & bottom).
+ const MARGIN_BODY_BOTTOM = 1; // margin on the body container.
+ const HEIGHT_QUESTION = 1; // The question text is one line.
+ const MARGIN_QUESTION_BOTTOM = 1; // Margin on the question container.
+ const HEIGHT_OPTIONS = options.length; // Each option in the radio select takes one line.
+
+ const surroundingElementsHeight =
+ PADDING_OUTER_Y +
+ MARGIN_BODY_BOTTOM +
+ HEIGHT_QUESTION +
+ MARGIN_QUESTION_BOTTOM +
+ HEIGHT_OPTIONS;
+ return Math.max(availableTerminalHeight - surroundingElementsHeight, 1);
+ }
+ if (confirmationDetails.type === 'edit') {
+ if (confirmationDetails.isModifying) {
+ return (
+
+ Modify in progress:
+
+ Save and close external editor to continue
+
+
+ );
+ }
+
+ question = `Apply this change?`;
+ options.push(
+ {
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ },
+ {
+ label: 'Yes, allow always',
+ value: ToolConfirmationOutcome.ProceedAlways,
+ },
+ {
+ label: 'Modify with external editor',
+ value: ToolConfirmationOutcome.ModifyWithEditor,
+ },
+ { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel },
+ );
+ bodyContent = (
+
+ );
+ } else if (confirmationDetails.type === 'exec') {
+ const executionProps =
+ confirmationDetails as ToolExecuteConfirmationDetails;
+
+ question = `Allow execution?`;
+ options.push(
+ {
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ },
+ {
+ label: `Yes, allow always "${executionProps.rootCommand} ..."`,
+ value: ToolConfirmationOutcome.ProceedAlways,
+ },
+ { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel },
+ );
+
+ let bodyContentHeight = availableBodyContentHeight();
+ if (bodyContentHeight !== undefined) {
+ bodyContentHeight -= 2; // Account for padding;
+ }
+ bodyContent = (
+
+
+
+
+ {executionProps.command}
+
+
+
+
+ );
+ } else if (confirmationDetails.type === 'info') {
+ const infoProps = confirmationDetails;
+ const displayUrls =
+ infoProps.urls &&
+ !(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt);
+
+ question = `Do you want to proceed?`;
+ options.push(
+ {
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ },
+ {
+ label: 'Yes, allow always',
+ value: ToolConfirmationOutcome.ProceedAlways,
+ },
+ { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel },
+ );
+
+ bodyContent = (
+
+ {infoProps.prompt}
+ {displayUrls && infoProps.urls && infoProps.urls.length > 0 && (
+
+ URLs to fetch:
+ {infoProps.urls.map((url) => (
+ - {url}
+ ))}
+
+ )}
+
+ );
+ } else {
+ // mcp tool confirmation
+ const mcpProps = confirmationDetails as ToolMcpConfirmationDetails;
+
+ bodyContent = (
+
+ MCP Server: {mcpProps.serverName}
+ Tool: {mcpProps.toolName}
+
+ );
+
+ question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
+ options.push(
+ {
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ },
+ {
+ label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
+ value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated
+ },
+ {
+ label: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
+ value: ToolConfirmationOutcome.ProceedAlwaysServer,
+ },
+ { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel },
+ );
+ }
+
+ return (
+
+ {/* Body Content (Diff Renderer or Command Info) */}
+ {/* No separate context display here anymore for edits */}
+
+ {bodyContent}
+
+
+ {/* Confirmation Question */}
+
+ {question}
+
+
+ {/* Select Input for Options */}
+
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
new file mode 100644
index 00000000..6352d361
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -0,0 +1,123 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useMemo } from 'react';
+import { Box } from 'ink';
+import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js';
+import { ToolMessage } from './ToolMessage.js';
+import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
+import { Colors } from '../../colors.js';
+import { Config } from '@qwen/qwen-code-core';
+
+interface ToolGroupMessageProps {
+ groupId: number;
+ toolCalls: IndividualToolCallDisplay[];
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+ config?: Config;
+ isFocused?: boolean;
+}
+
+// Main component renders the border and maps the tools using ToolMessage
+export const ToolGroupMessage: React.FC = ({
+ toolCalls,
+ availableTerminalHeight,
+ terminalWidth,
+ config,
+ isFocused = true,
+}) => {
+ const hasPending = !toolCalls.every(
+ (t) => t.status === ToolCallStatus.Success,
+ );
+ const borderColor = hasPending ? Colors.AccentYellow : Colors.Gray;
+
+ const staticHeight = /* border */ 2 + /* marginBottom */ 1;
+ // This is a bit of a magic number, but it accounts for the border and
+ // marginLeft.
+ const innerWidth = terminalWidth - 4;
+
+ // only prompt for tool approval on the first 'confirming' tool in the list
+ // note, after the CTA, this automatically moves over to the next 'confirming' tool
+ const toolAwaitingApproval = useMemo(
+ () => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
+ [toolCalls],
+ );
+
+ let countToolCallsWithResults = 0;
+ for (const tool of toolCalls) {
+ if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') {
+ countToolCallsWithResults++;
+ }
+ }
+ const countOneLineToolCalls = toolCalls.length - countToolCallsWithResults;
+ const availableTerminalHeightPerToolMessage = availableTerminalHeight
+ ? Math.max(
+ Math.floor(
+ (availableTerminalHeight - staticHeight - countOneLineToolCalls) /
+ Math.max(1, countToolCallsWithResults),
+ ),
+ 1,
+ )
+ : undefined;
+
+ return (
+
+ {toolCalls.map((tool) => {
+ const isConfirming = toolAwaitingApproval?.callId === tool.callId;
+ return (
+
+
+
+
+ {tool.status === ToolCallStatus.Confirming &&
+ isConfirming &&
+ tool.confirmationDetails && (
+
+ )}
+
+ );
+ })}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
new file mode 100644
index 00000000..7b9de92e
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
@@ -0,0 +1,181 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { render } from 'ink-testing-library';
+import { ToolMessage, ToolMessageProps } from './ToolMessage.js';
+import { StreamingState, ToolCallStatus } from '../../types.js';
+import { Text } from 'ink';
+import { StreamingContext } from '../../contexts/StreamingContext.js';
+
+// Mock child components or utilities if they are complex or have side effects
+vi.mock('../GeminiRespondingSpinner.js', () => ({
+ GeminiRespondingSpinner: ({
+ nonRespondingDisplay,
+ }: {
+ nonRespondingDisplay?: string;
+ }) => {
+ const streamingState = React.useContext(StreamingContext)!;
+ if (streamingState === StreamingState.Responding) {
+ return MockRespondingSpinner;
+ }
+ return nonRespondingDisplay ? {nonRespondingDisplay} : null;
+ },
+}));
+vi.mock('./DiffRenderer.js', () => ({
+ DiffRenderer: function MockDiffRenderer({
+ diffContent,
+ }: {
+ diffContent: string;
+ }) {
+ return MockDiff:{diffContent};
+ },
+}));
+vi.mock('../../utils/MarkdownDisplay.js', () => ({
+ MarkdownDisplay: function MockMarkdownDisplay({ text }: { text: string }) {
+ return MockMarkdown:{text};
+ },
+}));
+
+// Helper to render with context
+const renderWithContext = (
+ ui: React.ReactElement,
+ streamingState: StreamingState,
+) => {
+ const contextValue: StreamingState = streamingState;
+ return render(
+
+ {ui}
+ ,
+ );
+};
+
+describe('', () => {
+ const baseProps: ToolMessageProps = {
+ callId: 'tool-123',
+ name: 'test-tool',
+ description: 'A tool for testing',
+ resultDisplay: 'Test result',
+ status: ToolCallStatus.Success,
+ terminalWidth: 80,
+ confirmationDetails: undefined,
+ emphasis: 'medium',
+ };
+
+ it('renders basic tool information', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ const output = lastFrame();
+ expect(output).toContain('โ'); // Success indicator
+ expect(output).toContain('test-tool');
+ expect(output).toContain('A tool for testing');
+ expect(output).toContain('MockMarkdown:Test result');
+ });
+
+ describe('ToolStatusIndicator rendering', () => {
+ it('shows โ for Success status', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ expect(lastFrame()).toContain('โ');
+ });
+
+ it('shows o for Pending status', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ expect(lastFrame()).toContain('o');
+ });
+
+ it('shows ? for Confirming status', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ expect(lastFrame()).toContain('?');
+ });
+
+ it('shows - for Canceled status', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ expect(lastFrame()).toContain('-');
+ });
+
+ it('shows x for Error status', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ expect(lastFrame()).toContain('x');
+ });
+
+ it('shows paused spinner for Executing status when streamingState is Idle', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ expect(lastFrame()).toContain('โท');
+ expect(lastFrame()).not.toContain('MockRespondingSpinner');
+ expect(lastFrame()).not.toContain('โ');
+ });
+
+ it('shows paused spinner for Executing status when streamingState is WaitingForConfirmation', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.WaitingForConfirmation,
+ );
+ expect(lastFrame()).toContain('โท');
+ expect(lastFrame()).not.toContain('MockRespondingSpinner');
+ expect(lastFrame()).not.toContain('โ');
+ });
+
+ it('shows MockRespondingSpinner for Executing status when streamingState is Responding', () => {
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Responding, // Simulate app still responding
+ );
+ expect(lastFrame()).toContain('MockRespondingSpinner');
+ expect(lastFrame()).not.toContain('โ');
+ });
+ });
+
+ it('renders DiffRenderer for diff results', () => {
+ const diffResult = {
+ fileDiff: '--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-old\n+new',
+ fileName: 'file.txt',
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ // Check that the output contains the MockDiff content as part of the whole message
+ expect(lastFrame()).toMatch(/MockDiff:--- a\/file\.txt/);
+ });
+
+ it('renders emphasis correctly', () => {
+ const { lastFrame: highEmphasisFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ // Check for trailing indicator or specific color if applicable (Colors are not easily testable here)
+ expect(highEmphasisFrame()).toContain('โ'); // Trailing indicator for high emphasis
+
+ const { lastFrame: lowEmphasisFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+ // For low emphasis, the name and description might be dimmed (check for dimColor if possible)
+ // This is harder to assert directly in text output without color checks.
+ // We can at least ensure it doesn't have the high emphasis indicator.
+ expect(lowEmphasisFrame()).not.toContain('โ');
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx
new file mode 100644
index 00000000..e1eb75b8
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx
@@ -0,0 +1,194 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js';
+import { DiffRenderer } from './DiffRenderer.js';
+import { Colors } from '../../colors.js';
+import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
+import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
+import { MaxSizedBox } from '../shared/MaxSizedBox.js';
+
+const STATIC_HEIGHT = 1;
+const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
+const STATUS_INDICATOR_WIDTH = 3;
+const MIN_LINES_SHOWN = 2; // show at least this many lines
+
+// Large threshold to ensure we don't cause performance issues for very large
+// outputs that will get truncated further MaxSizedBox anyway.
+const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 1000000;
+export type TextEmphasis = 'high' | 'medium' | 'low';
+
+export interface ToolMessageProps extends IndividualToolCallDisplay {
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+ emphasis?: TextEmphasis;
+ renderOutputAsMarkdown?: boolean;
+}
+
+export const ToolMessage: React.FC = ({
+ name,
+ description,
+ resultDisplay,
+ status,
+ availableTerminalHeight,
+ terminalWidth,
+ emphasis = 'medium',
+ renderOutputAsMarkdown = true,
+}) => {
+ const availableHeight = availableTerminalHeight
+ ? Math.max(
+ availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT,
+ MIN_LINES_SHOWN + 1, // enforce minimum lines shown
+ )
+ : undefined;
+
+ // Long tool call response in MarkdownDisplay doesn't respect availableTerminalHeight properly,
+ // we're forcing it to not render as markdown when the response is too long, it will fallback
+ // to render as plain text, which is contained within the terminal using MaxSizedBox
+ if (availableHeight) {
+ renderOutputAsMarkdown = false;
+ }
+
+ const childWidth = terminalWidth - 3; // account for padding.
+ if (typeof resultDisplay === 'string') {
+ if (resultDisplay.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
+ // Truncate the result display to fit within the available width.
+ resultDisplay =
+ '...' + resultDisplay.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
+ }
+ }
+ return (
+
+
+
+
+ {emphasis === 'high' && }
+
+ {resultDisplay && (
+
+
+ {typeof resultDisplay === 'string' && renderOutputAsMarkdown && (
+
+
+
+ )}
+ {typeof resultDisplay === 'string' && !renderOutputAsMarkdown && (
+
+
+ {resultDisplay}
+
+
+ )}
+ {typeof resultDisplay !== 'string' && (
+
+ )}
+
+
+ )}
+
+ );
+};
+
+type ToolStatusIndicatorProps = {
+ status: ToolCallStatus;
+};
+
+const ToolStatusIndicator: React.FC = ({
+ status,
+}) => (
+
+ {status === ToolCallStatus.Pending && (
+ o
+ )}
+ {status === ToolCallStatus.Executing && (
+
+ )}
+ {status === ToolCallStatus.Success && (
+ โ
+ )}
+ {status === ToolCallStatus.Confirming && (
+ ?
+ )}
+ {status === ToolCallStatus.Canceled && (
+
+ -
+
+ )}
+ {status === ToolCallStatus.Error && (
+
+ x
+
+ )}
+
+);
+
+type ToolInfo = {
+ name: string;
+ description: string;
+ status: ToolCallStatus;
+ emphasis: TextEmphasis;
+};
+const ToolInfo: React.FC = ({
+ name,
+ description,
+ status,
+ emphasis,
+}) => {
+ const nameColor = React.useMemo(() => {
+ switch (emphasis) {
+ case 'high':
+ return Colors.Foreground;
+ case 'medium':
+ return Colors.Foreground;
+ case 'low':
+ return Colors.Gray;
+ default: {
+ const exhaustiveCheck: never = emphasis;
+ return exhaustiveCheck;
+ }
+ }
+ }, [emphasis]);
+ return (
+
+
+
+ {name}
+ {' '}
+ {description}
+
+
+ );
+};
+
+const TrailingIndicator: React.FC = () => (
+
+ {' '}
+ โ
+
+);
diff --git a/packages/cli/src/ui/components/messages/UserMessage.tsx b/packages/cli/src/ui/components/messages/UserMessage.tsx
new file mode 100644
index 00000000..46f3d4a2
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/UserMessage.tsx
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Text, Box } from 'ink';
+import { Colors } from '../../colors.js';
+
+interface UserMessageProps {
+ text: string;
+}
+
+export const UserMessage: React.FC = ({ text }) => {
+ const prefix = '> ';
+ const prefixWidth = prefix.length;
+
+ return (
+
+
+ {prefix}
+
+
+
+ {text}
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/UserShellMessage.tsx b/packages/cli/src/ui/components/messages/UserShellMessage.tsx
new file mode 100644
index 00000000..946ca7e7
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/UserShellMessage.tsx
@@ -0,0 +1,25 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../../colors.js';
+
+interface UserShellMessageProps {
+ text: string;
+}
+
+export const UserShellMessage: React.FC = ({ text }) => {
+ // Remove leading '!' if present, as App.tsx adds it for the processor.
+ const commandToDisplay = text.startsWith('!') ? text.substring(1) : text;
+
+ return (
+
+ $
+ {commandToDisplay}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
new file mode 100644
index 00000000..50951b4f
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
@@ -0,0 +1,342 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import { OverflowProvider } from '../../contexts/OverflowContext.js';
+import { MaxSizedBox, setMaxSizedBoxDebugging } from './MaxSizedBox.js';
+import { Box, Text } from 'ink';
+import { describe, it, expect } from 'vitest';
+
+describe('', () => {
+ // Make sure MaxSizedBox logs errors on invalid configurations.
+ // This is useful for debugging issues with the component.
+ // It should be set to false in production for performance and to avoid
+ // cluttering the console if there are ignorable issues.
+ setMaxSizedBoxDebugging(true);
+
+ it('renders children without truncation when they fit', () => {
+ const { lastFrame } = render(
+
+
+
+ Hello, World!
+
+
+ ,
+ );
+ expect(lastFrame()).equals('Hello, World!');
+ });
+
+ it('hides lines when content exceeds maxHeight', () => {
+ const { lastFrame } = render(
+
+
+
+ Line 1
+
+
+ Line 2
+
+
+ Line 3
+
+
+ ,
+ );
+ expect(lastFrame()).equals(`... first 2 lines hidden ...
+Line 3`);
+ });
+
+ it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', () => {
+ const { lastFrame } = render(
+
+
+
+ Line 1
+
+
+ Line 2
+
+
+ Line 3
+
+
+ ,
+ );
+ expect(lastFrame()).equals(`Line 1
+... last 2 lines hidden ...`);
+ });
+
+ it('wraps text that exceeds maxWidth', () => {
+ const { lastFrame } = render(
+
+
+
+ This is a long line of text
+
+
+ ,
+ );
+
+ expect(lastFrame()).equals(`This is a
+long line
+of text`);
+ });
+
+ it('handles mixed wrapping and non-wrapping segments', () => {
+ const multilineText = `This part will wrap around.
+And has a line break.
+ Leading spaces preserved.`;
+ const { lastFrame } = render(
+
+
+
+ Example
+
+
+ No Wrap:
+ {multilineText}
+
+
+ Longer No Wrap:
+ This part will wrap around.
+
+
+ ,
+ );
+
+ expect(lastFrame()).equals(
+ `Example
+No Wrap: This part
+ will wrap
+ around.
+ And has a
+ line break.
+ Leading
+ spaces
+ preserved.
+Longer No Wrap: This
+ part
+ will
+ wrap
+ arou
+ nd.`,
+ );
+ });
+
+ it('handles words longer than maxWidth by splitting them', () => {
+ const { lastFrame } = render(
+
+
+
+ Supercalifragilisticexpialidocious
+
+
+ ,
+ );
+
+ expect(lastFrame()).equals(`... โฆ
+istic
+expia
+lidoc
+ious`);
+ });
+
+ it('does not truncate when maxHeight is undefined', () => {
+ const { lastFrame } = render(
+
+
+
+ Line 1
+
+
+ Line 2
+
+
+ ,
+ );
+ expect(lastFrame()).equals(`Line 1
+Line 2`);
+ });
+
+ it('shows plural "lines" when more than one line is hidden', () => {
+ const { lastFrame } = render(
+
+
+
+ Line 1
+
+
+ Line 2
+
+
+ Line 3
+
+
+ ,
+ );
+ expect(lastFrame()).equals(`... first 2 lines hidden ...
+Line 3`);
+ });
+
+ it('shows plural "lines" when more than one line is hidden and overflowDirection is bottom', () => {
+ const { lastFrame } = render(
+
+
+
+ Line 1
+
+
+ Line 2
+
+
+ Line 3
+
+
+ ,
+ );
+ expect(lastFrame()).equals(`Line 1
+... last 2 lines hidden ...`);
+ });
+
+ it('renders an empty box for empty children', () => {
+ const { lastFrame } = render(
+
+
+ ,
+ );
+ // Expect an empty string or a box with nothing in it.
+ // Ink renders an empty box as an empty string.
+ expect(lastFrame()).equals('');
+ });
+
+ it('wraps text with multi-byte unicode characters correctly', () => {
+ const { lastFrame } = render(
+
+
+
+ ไฝ ๅฅฝไธ็
+
+
+ ,
+ );
+
+ // "ไฝ ๅฅฝ" has a visual width of 4. "ไธ็" has a visual width of 4.
+ // With maxWidth=5, it should wrap after the second character.
+ expect(lastFrame()).equals(`ไฝ ๅฅฝ
+ไธ็`);
+ });
+
+ it('wraps text with multi-byte emoji characters correctly', () => {
+ const { lastFrame } = render(
+
+
+
+ ๐ถ๐ถ๐ถ๐ถ๐ถ
+
+
+ ,
+ );
+
+ // Each "๐ถ" has a visual width of 2.
+ // With maxWidth=5, it should wrap every 2 emojis.
+ expect(lastFrame()).equals(`๐ถ๐ถ
+๐ถ๐ถ
+๐ถ`);
+ });
+
+ it('accounts for additionalHiddenLinesCount', () => {
+ const { lastFrame } = render(
+
+
+
+ Line 1
+
+
+ Line 2
+
+
+ Line 3
+
+
+ ,
+ );
+ // 1 line is hidden by overflow, 5 are additionally hidden.
+ expect(lastFrame()).equals(`... first 7 lines hidden ...
+Line 3`);
+ });
+
+ it('handles React.Fragment as a child', () => {
+ const { lastFrame } = render(
+
+
+ <>
+
+ Line 1 from Fragment
+
+
+ Line 2 from Fragment
+
+ >
+
+ Line 3 direct child
+
+
+ ,
+ );
+ expect(lastFrame()).equals(`Line 1 from Fragment
+Line 2 from Fragment
+Line 3 direct child`);
+ });
+
+ it('clips a long single text child from the top', () => {
+ const THIRTY_LINES = Array.from(
+ { length: 30 },
+ (_, i) => `Line ${i + 1}`,
+ ).join('\n');
+
+ const { lastFrame } = render(
+
+
+
+ {THIRTY_LINES}
+
+
+ ,
+ );
+
+ const expected = [
+ '... first 21 lines hidden ...',
+ ...Array.from({ length: 9 }, (_, i) => `Line ${22 + i}`),
+ ].join('\n');
+
+ expect(lastFrame()).equals(expected);
+ });
+
+ it('clips a long single text child from the bottom', () => {
+ const THIRTY_LINES = Array.from(
+ { length: 30 },
+ (_, i) => `Line ${i + 1}`,
+ ).join('\n');
+
+ const { lastFrame } = render(
+
+
+
+ {THIRTY_LINES}
+
+
+ ,
+ );
+
+ const expected = [
+ ...Array.from({ length: 9 }, (_, i) => `Line ${i + 1}`),
+ '... last 21 lines hidden ...',
+ ].join('\n');
+
+ expect(lastFrame()).equals(expected);
+ });
+});
diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
new file mode 100644
index 00000000..eb5ef6b4
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
@@ -0,0 +1,547 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Fragment, useEffect, useId } from 'react';
+import { Box, Text } from 'ink';
+import stringWidth from 'string-width';
+import { Colors } from '../../colors.js';
+import { toCodePoints } from '../../utils/textUtils.js';
+import { useOverflowActions } from '../../contexts/OverflowContext.js';
+
+let enableDebugLog = false;
+
+/**
+ * Minimum height for the MaxSizedBox component.
+ * This ensures there is room for at least one line of content as well as the
+ * message that content was truncated.
+ */
+export const MINIMUM_MAX_HEIGHT = 2;
+
+export function setMaxSizedBoxDebugging(value: boolean) {
+ enableDebugLog = value;
+}
+
+function debugReportError(message: string, element: React.ReactNode) {
+ if (!enableDebugLog) return;
+
+ if (!React.isValidElement(element)) {
+ console.error(
+ message,
+ `Invalid element: '${String(element)}' typeof=${typeof element}`,
+ );
+ return;
+ }
+
+ let sourceMessage = '';
+ try {
+ const elementWithSource = element as {
+ _source?: { fileName?: string; lineNumber?: number };
+ };
+ const fileName = elementWithSource._source?.fileName;
+ const lineNumber = elementWithSource._source?.lineNumber;
+ sourceMessage = fileName ? `${fileName}:${lineNumber}` : '';
+ } catch (error) {
+ console.error('Error while trying to get file name:', error);
+ }
+
+ console.error(message, `${String(element.type)}. Source: ${sourceMessage}`);
+}
+interface MaxSizedBoxProps {
+ children?: React.ReactNode;
+ maxWidth?: number;
+ maxHeight: number | undefined;
+ overflowDirection?: 'top' | 'bottom';
+ additionalHiddenLinesCount?: number;
+}
+
+/**
+ * A React component that constrains the size of its children and provides
+ * content-aware truncation when the content exceeds the specified `maxHeight`.
+ *
+ * `MaxSizedBox` requires a specific structure for its children to correctly
+ * measure and render the content:
+ *
+ * 1. **Direct children must be `` elements.** Each `` represents a
+ * single row of content.
+ * 2. **Row `` elements must contain only `` elements.** These
+ * `` elements can be nested and there are no restrictions to Text
+ * element styling other than that non-wrapping text elements must be
+ * before wrapping text elements.
+ *
+ * **Constraints:**
+ * - **Box Properties:** Custom properties on the child `` elements are
+ * ignored. In debug mode, runtime checks will report errors for any
+ * unsupported properties.
+ * - **Text Wrapping:** Within a single row, `` elements with no wrapping
+ * (e.g., headers, labels) must appear before any `` elements that wrap.
+ * - **Element Types:** Runtime checks will warn if unsupported element types
+ * are used as children.
+ *
+ * @example
+ *
+ *
+ * This is the first line.
+ *
+ *
+ * Non-wrapping Header:
+ * This is the rest of the line which will wrap if it's too long.
+ *
+ *
+ *
+ * Line 3 with nested styled text inside of it.
+ *
+ *
+ *
+ */
+export const MaxSizedBox: React.FC = ({
+ children,
+ maxWidth,
+ maxHeight,
+ overflowDirection = 'top',
+ additionalHiddenLinesCount = 0,
+}) => {
+ const id = useId();
+ const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {};
+
+ const laidOutStyledText: StyledText[][] = [];
+ const targetMaxHeight = Math.max(
+ Math.round(maxHeight ?? Number.MAX_SAFE_INTEGER),
+ MINIMUM_MAX_HEIGHT,
+ );
+
+ if (maxWidth === undefined) {
+ throw new Error('maxWidth must be defined when maxHeight is set.');
+ }
+ function visitRows(element: React.ReactNode) {
+ if (!React.isValidElement<{ children?: React.ReactNode }>(element)) {
+ return;
+ }
+
+ if (element.type === Fragment) {
+ React.Children.forEach(element.props.children, visitRows);
+ return;
+ }
+
+ if (element.type === Box) {
+ layoutInkElementAsStyledText(element, maxWidth!, laidOutStyledText);
+ return;
+ }
+
+ debugReportError('MaxSizedBox children must be elements', element);
+ }
+
+ React.Children.forEach(children, visitRows);
+
+ const contentWillOverflow =
+ (targetMaxHeight !== undefined &&
+ laidOutStyledText.length > targetMaxHeight) ||
+ additionalHiddenLinesCount > 0;
+ const visibleContentHeight =
+ contentWillOverflow && targetMaxHeight !== undefined
+ ? targetMaxHeight - 1
+ : targetMaxHeight;
+
+ const hiddenLinesCount =
+ visibleContentHeight !== undefined
+ ? Math.max(0, laidOutStyledText.length - visibleContentHeight)
+ : 0;
+ const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount;
+
+ useEffect(() => {
+ if (totalHiddenLines > 0) {
+ addOverflowingId?.(id);
+ } else {
+ removeOverflowingId?.(id);
+ }
+
+ return () => {
+ removeOverflowingId?.(id);
+ };
+ }, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]);
+
+ const visibleStyledText =
+ hiddenLinesCount > 0
+ ? overflowDirection === 'top'
+ ? laidOutStyledText.slice(hiddenLinesCount, laidOutStyledText.length)
+ : laidOutStyledText.slice(0, visibleContentHeight)
+ : laidOutStyledText;
+
+ const visibleLines = visibleStyledText.map((line, index) => (
+
+ {line.length > 0 ? (
+ line.map((segment, segIndex) => (
+
+ {segment.text}
+
+ ))
+ ) : (
+
+ )}
+
+ ));
+
+ return (
+
+ {totalHiddenLines > 0 && overflowDirection === 'top' && (
+
+ ... first {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
+ hidden ...
+
+ )}
+ {visibleLines}
+ {totalHiddenLines > 0 && overflowDirection === 'bottom' && (
+
+ ... last {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
+ hidden ...
+
+ )}
+
+ );
+};
+
+// Define a type for styled text segments
+interface StyledText {
+ text: string;
+ props: Record;
+}
+
+/**
+ * Single row of content within the MaxSizedBox.
+ *
+ * A row can contain segments that are not wrapped, followed by segments that
+ * are. This is a minimal implementation that only supports the functionality
+ * needed today.
+ */
+interface Row {
+ noWrapSegments: StyledText[];
+ segments: StyledText[];
+}
+
+/**
+ * Flattens the child elements of MaxSizedBox into an array of `Row` objects.
+ *
+ * This function expects a specific child structure to function correctly:
+ * 1. The top-level child of `MaxSizedBox` should be a single ``. This
+ * outer box is primarily for structure and is not directly rendered.
+ * 2. Inside the outer ``, there should be one or more children. Each of
+ * these children must be a `` that represents a row.
+ * 3. Inside each "row" ``, the children must be `` components.
+ *
+ * The structure should look like this:
+ *
+ * // Row 1
+ * ...
+ * ...
+ *
+ * // Row 2
+ * ...
+ *
+ *
+ *
+ * It is an error for a child without wrapping to appear after a
+ * child with wrapping within the same row Box.
+ *
+ * @param element The React node to flatten.
+ * @returns An array of `Row` objects.
+ */
+function visitBoxRow(element: React.ReactNode): Row {
+ if (
+ !React.isValidElement<{ children?: React.ReactNode }>(element) ||
+ element.type !== Box
+ ) {
+ debugReportError(
+ `All children of MaxSizedBox must be elements`,
+ element,
+ );
+ return {
+ noWrapSegments: [{ text: '', props: {} }],
+ segments: [],
+ };
+ }
+
+ if (enableDebugLog) {
+ const boxProps = element.props as {
+ children?: React.ReactNode | undefined;
+ readonly flexDirection?:
+ | 'row'
+ | 'column'
+ | 'row-reverse'
+ | 'column-reverse'
+ | undefined;
+ };
+ // Ensure the Box has no props other than the default ones and key.
+ let maxExpectedProps = 4;
+ if (boxProps.children !== undefined) {
+ // Allow the key prop, which is automatically added by React.
+ maxExpectedProps += 1;
+ }
+ if (
+ boxProps.flexDirection !== undefined &&
+ boxProps.flexDirection !== 'row'
+ ) {
+ debugReportError(
+ 'MaxSizedBox children must have flexDirection="row".',
+ element,
+ );
+ }
+ if (Object.keys(boxProps).length > maxExpectedProps) {
+ debugReportError(
+ `Boxes inside MaxSizedBox must not have additional props. ${Object.keys(
+ boxProps,
+ ).join(', ')}`,
+ element,
+ );
+ }
+ }
+
+ const row: Row = {
+ noWrapSegments: [],
+ segments: [],
+ };
+
+ let hasSeenWrapped = false;
+
+ function visitRowChild(
+ element: React.ReactNode,
+ parentProps: Record | undefined,
+ ) {
+ if (element === null) {
+ return;
+ }
+ if (typeof element === 'string' || typeof element === 'number') {
+ const text = String(element);
+ // Ignore empty strings as they don't need to be rendered.
+ if (!text) {
+ return;
+ }
+
+ const segment: StyledText = { text, props: parentProps ?? {} };
+
+ // Check the 'wrap' property from the merged props to decide the segment type.
+ if (parentProps === undefined || parentProps.wrap === 'wrap') {
+ hasSeenWrapped = true;
+ row.segments.push(segment);
+ } else {
+ if (!hasSeenWrapped) {
+ row.noWrapSegments.push(segment);
+ } else {
+ // put in the wrapped segment as the row is already stuck in wrapped mode.
+ row.segments.push(segment);
+ debugReportError(
+ 'Text elements without wrapping cannot appear after elements with wrapping in the same row.',
+ element,
+ );
+ }
+ }
+ return;
+ }
+
+ if (!React.isValidElement<{ children?: React.ReactNode }>(element)) {
+ debugReportError('Invalid element.', element);
+ return;
+ }
+
+ if (element.type === Fragment) {
+ React.Children.forEach(element.props.children, (child) =>
+ visitRowChild(child, parentProps),
+ );
+ return;
+ }
+
+ if (element.type !== Text) {
+ debugReportError(
+ 'Children of a row Box must be elements.',
+ element,
+ );
+ return;
+ }
+
+ // Merge props from parent elements. Child props take precedence.
+ const { children, ...currentProps } = element.props;
+ const mergedProps =
+ parentProps === undefined
+ ? currentProps
+ : { ...parentProps, ...currentProps };
+ React.Children.forEach(children, (child) =>
+ visitRowChild(child, mergedProps),
+ );
+ }
+
+ React.Children.forEach(element.props.children, (child) =>
+ visitRowChild(child, undefined),
+ );
+
+ return row;
+}
+
+function layoutInkElementAsStyledText(
+ element: React.ReactElement,
+ maxWidth: number,
+ output: StyledText[][],
+) {
+ const row = visitBoxRow(element);
+ if (row.segments.length === 0 && row.noWrapSegments.length === 0) {
+ // Return a single empty line if there are no segments to display
+ output.push([]);
+ return;
+ }
+
+ const lines: StyledText[][] = [];
+ const nonWrappingContent: StyledText[] = [];
+ let noWrappingWidth = 0;
+
+ // First, lay out the non-wrapping segments
+ row.noWrapSegments.forEach((segment) => {
+ nonWrappingContent.push(segment);
+ noWrappingWidth += stringWidth(segment.text);
+ });
+
+ if (row.segments.length === 0) {
+ // This is a bit of a special case when there are no segments that allow
+ // wrapping. It would be ideal to unify.
+ const lines: StyledText[][] = [];
+ let currentLine: StyledText[] = [];
+ nonWrappingContent.forEach((segment) => {
+ const textLines = segment.text.split('\n');
+ textLines.forEach((text, index) => {
+ if (index > 0) {
+ lines.push(currentLine);
+ currentLine = [];
+ }
+ if (text) {
+ currentLine.push({ text, props: segment.props });
+ }
+ });
+ });
+ if (
+ currentLine.length > 0 ||
+ (nonWrappingContent.length > 0 &&
+ nonWrappingContent[nonWrappingContent.length - 1].text.endsWith('\n'))
+ ) {
+ lines.push(currentLine);
+ }
+ for (const line of lines) {
+ output.push(line);
+ }
+ return;
+ }
+
+ const availableWidth = maxWidth - noWrappingWidth;
+
+ if (availableWidth < 1) {
+ // No room to render the wrapping segments. TODO(jacob314): consider an alternative fallback strategy.
+ output.push(nonWrappingContent);
+ return;
+ }
+
+ // Now, lay out the wrapping segments
+ let wrappingPart: StyledText[] = [];
+ let wrappingPartWidth = 0;
+
+ function addWrappingPartToLines() {
+ if (lines.length === 0) {
+ lines.push([...nonWrappingContent, ...wrappingPart]);
+ } else {
+ if (noWrappingWidth > 0) {
+ lines.push([
+ ...[{ text: ' '.repeat(noWrappingWidth), props: {} }],
+ ...wrappingPart,
+ ]);
+ } else {
+ lines.push(wrappingPart);
+ }
+ }
+ wrappingPart = [];
+ wrappingPartWidth = 0;
+ }
+
+ function addToWrappingPart(text: string, props: Record) {
+ if (
+ wrappingPart.length > 0 &&
+ wrappingPart[wrappingPart.length - 1].props === props
+ ) {
+ wrappingPart[wrappingPart.length - 1].text += text;
+ } else {
+ wrappingPart.push({ text, props });
+ }
+ }
+
+ row.segments.forEach((segment) => {
+ const linesFromSegment = segment.text.split('\n');
+
+ linesFromSegment.forEach((lineText, lineIndex) => {
+ if (lineIndex > 0) {
+ addWrappingPartToLines();
+ }
+
+ const words = lineText.split(/(\s+)/); // Split by whitespace
+
+ words.forEach((word) => {
+ if (!word) return;
+ const wordWidth = stringWidth(word);
+
+ if (
+ wrappingPartWidth + wordWidth > availableWidth &&
+ wrappingPartWidth > 0
+ ) {
+ addWrappingPartToLines();
+ if (/^\s+$/.test(word)) {
+ return;
+ }
+ }
+
+ if (wordWidth > availableWidth) {
+ // Word is too long, needs to be split across lines
+ const wordAsCodePoints = toCodePoints(word);
+ let remainingWordAsCodePoints = wordAsCodePoints;
+ while (remainingWordAsCodePoints.length > 0) {
+ let splitIndex = 0;
+ let currentSplitWidth = 0;
+ for (const char of remainingWordAsCodePoints) {
+ const charWidth = stringWidth(char);
+ if (
+ wrappingPartWidth + currentSplitWidth + charWidth >
+ availableWidth
+ ) {
+ break;
+ }
+ currentSplitWidth += charWidth;
+ splitIndex++;
+ }
+
+ if (splitIndex > 0) {
+ const part = remainingWordAsCodePoints
+ .slice(0, splitIndex)
+ .join('');
+ addToWrappingPart(part, segment.props);
+ wrappingPartWidth += stringWidth(part);
+ remainingWordAsCodePoints =
+ remainingWordAsCodePoints.slice(splitIndex);
+ }
+
+ if (remainingWordAsCodePoints.length > 0) {
+ addWrappingPartToLines();
+ }
+ }
+ } else {
+ addToWrappingPart(word, segment.props);
+ wrappingPartWidth += wordWidth;
+ }
+ });
+ });
+ // Split omits a trailing newline, so we need to handle it here
+ if (segment.text.endsWith('\n')) {
+ addWrappingPartToLines();
+ }
+ });
+
+ if (wrappingPart.length > 0) {
+ addWrappingPartToLines();
+ }
+ for (const line of lines) {
+ output.push(line);
+ }
+}
diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
new file mode 100644
index 00000000..499c136a
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
@@ -0,0 +1,157 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useEffect, useState } from 'react';
+import { Text, Box, useInput } from 'ink';
+import { Colors } from '../../colors.js';
+
+/**
+ * Represents a single option for the RadioButtonSelect.
+ * Requires a label for display and a value to be returned on selection.
+ */
+export interface RadioSelectItem {
+ label: string;
+ value: T;
+ disabled?: boolean;
+ themeNameDisplay?: string;
+ themeTypeDisplay?: string;
+}
+
+/**
+ * Props for the RadioButtonSelect component.
+ * @template T The type of the value associated with each radio item.
+ */
+export interface RadioButtonSelectProps {
+ /** An array of items to display as radio options. */
+ items: Array>;
+ /** The initial index selected */
+ initialIndex?: number;
+ /** Function called when an item is selected. Receives the `value` of the selected item. */
+ onSelect: (value: T) => void;
+ /** Function called when an item is highlighted. Receives the `value` of the selected item. */
+ onHighlight?: (value: T) => void;
+ /** Whether this select input is currently focused and should respond to input. */
+ isFocused?: boolean;
+ /** Whether to show the scroll arrows. */
+ showScrollArrows?: boolean;
+ /** The maximum number of items to show at once. */
+ maxItemsToShow?: number;
+}
+
+/**
+ * A custom component that displays a list of items with radio buttons,
+ * supporting scrolling and keyboard navigation.
+ *
+ * @template T The type of the value associated with each radio item.
+ */
+export function RadioButtonSelect({
+ items,
+ initialIndex = 0,
+ onSelect,
+ onHighlight,
+ isFocused,
+ showScrollArrows = false,
+ maxItemsToShow = 10,
+}: RadioButtonSelectProps): React.JSX.Element {
+ const [activeIndex, setActiveIndex] = useState(initialIndex);
+ const [scrollOffset, setScrollOffset] = useState(0);
+
+ useEffect(() => {
+ const newScrollOffset = Math.max(
+ 0,
+ Math.min(activeIndex - maxItemsToShow + 1, items.length - maxItemsToShow),
+ );
+ if (activeIndex < scrollOffset) {
+ setScrollOffset(activeIndex);
+ } else if (activeIndex >= scrollOffset + maxItemsToShow) {
+ setScrollOffset(newScrollOffset);
+ }
+ }, [activeIndex, items.length, scrollOffset, maxItemsToShow]);
+
+ useInput(
+ (input, key) => {
+ if (input === 'k' || key.upArrow) {
+ const newIndex = activeIndex > 0 ? activeIndex - 1 : items.length - 1;
+ setActiveIndex(newIndex);
+ onHighlight?.(items[newIndex]!.value);
+ }
+ if (input === 'j' || key.downArrow) {
+ const newIndex = activeIndex < items.length - 1 ? activeIndex + 1 : 0;
+ setActiveIndex(newIndex);
+ onHighlight?.(items[newIndex]!.value);
+ }
+ if (key.return) {
+ onSelect(items[activeIndex]!.value);
+ }
+
+ // Enable selection directly from number keys.
+ if (/^[1-9]$/.test(input)) {
+ const targetIndex = Number.parseInt(input, 10) - 1;
+ if (targetIndex >= 0 && targetIndex < visibleItems.length) {
+ const selectedItem = visibleItems[targetIndex];
+ if (selectedItem) {
+ onSelect?.(selectedItem.value);
+ }
+ }
+ }
+ },
+ { isActive: isFocused && items.length > 0 },
+ );
+
+ const visibleItems = items.slice(scrollOffset, scrollOffset + maxItemsToShow);
+
+ return (
+
+ {showScrollArrows && (
+ 0 ? Colors.Foreground : Colors.Gray}>
+ โฒ
+
+ )}
+ {visibleItems.map((item, index) => {
+ const itemIndex = scrollOffset + index;
+ const isSelected = activeIndex === itemIndex;
+
+ let textColor = Colors.Foreground;
+ if (isSelected) {
+ textColor = Colors.AccentGreen;
+ } else if (item.disabled) {
+ textColor = Colors.Gray;
+ }
+
+ return (
+
+
+
+ {isSelected ? 'โ' : 'โ'}
+
+
+ {item.themeNameDisplay && item.themeTypeDisplay ? (
+
+ {item.themeNameDisplay}{' '}
+ {item.themeTypeDisplay}
+
+ ) : (
+
+ {item.label}
+
+ )}
+
+ );
+ })}
+ {showScrollArrows && (
+
+ โผ
+
+ )}
+
+ );
+}
diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts
new file mode 100644
index 00000000..89930c18
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts
@@ -0,0 +1,1340 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import {
+ useTextBuffer,
+ Viewport,
+ TextBuffer,
+ offsetToLogicalPos,
+ textBufferReducer,
+ TextBufferState,
+ TextBufferAction,
+} from './text-buffer.js';
+
+const initialState: TextBufferState = {
+ lines: [''],
+ cursorRow: 0,
+ cursorCol: 0,
+ preferredCol: null,
+ undoStack: [],
+ redoStack: [],
+ clipboard: null,
+ selectionAnchor: null,
+};
+
+describe('textBufferReducer', () => {
+ it('should return the initial state if state is undefined', () => {
+ const action = { type: 'unknown_action' } as unknown as TextBufferAction;
+ const state = textBufferReducer(initialState, action);
+ expect(state).toEqual(initialState);
+ });
+
+ describe('set_text action', () => {
+ it('should set new text and move cursor to the end', () => {
+ const action: TextBufferAction = {
+ type: 'set_text',
+ payload: 'hello\nworld',
+ };
+ const state = textBufferReducer(initialState, action);
+ expect(state.lines).toEqual(['hello', 'world']);
+ expect(state.cursorRow).toBe(1);
+ expect(state.cursorCol).toBe(5);
+ expect(state.undoStack.length).toBe(1);
+ });
+
+ it('should not create an undo snapshot if pushToUndo is false', () => {
+ const action: TextBufferAction = {
+ type: 'set_text',
+ payload: 'no undo',
+ pushToUndo: false,
+ };
+ const state = textBufferReducer(initialState, action);
+ expect(state.lines).toEqual(['no undo']);
+ expect(state.undoStack.length).toBe(0);
+ });
+ });
+
+ describe('insert action', () => {
+ it('should insert a character', () => {
+ const action: TextBufferAction = { type: 'insert', payload: 'a' };
+ const state = textBufferReducer(initialState, action);
+ expect(state.lines).toEqual(['a']);
+ expect(state.cursorCol).toBe(1);
+ });
+
+ it('should insert a newline', () => {
+ const stateWithText = { ...initialState, lines: ['hello'] };
+ const action: TextBufferAction = { type: 'insert', payload: '\n' };
+ const state = textBufferReducer(stateWithText, action);
+ expect(state.lines).toEqual(['', 'hello']);
+ expect(state.cursorRow).toBe(1);
+ expect(state.cursorCol).toBe(0);
+ });
+ });
+
+ describe('backspace action', () => {
+ it('should remove a character', () => {
+ const stateWithText: TextBufferState = {
+ ...initialState,
+ lines: ['a'],
+ cursorRow: 0,
+ cursorCol: 1,
+ };
+ const action: TextBufferAction = { type: 'backspace' };
+ const state = textBufferReducer(stateWithText, action);
+ expect(state.lines).toEqual(['']);
+ expect(state.cursorCol).toBe(0);
+ });
+
+ it('should join lines if at the beginning of a line', () => {
+ const stateWithText: TextBufferState = {
+ ...initialState,
+ lines: ['hello', 'world'],
+ cursorRow: 1,
+ cursorCol: 0,
+ };
+ const action: TextBufferAction = { type: 'backspace' };
+ const state = textBufferReducer(stateWithText, action);
+ expect(state.lines).toEqual(['helloworld']);
+ expect(state.cursorRow).toBe(0);
+ expect(state.cursorCol).toBe(5);
+ });
+ });
+
+ describe('undo/redo actions', () => {
+ it('should undo and redo a change', () => {
+ // 1. Insert text
+ const insertAction: TextBufferAction = {
+ type: 'insert',
+ payload: 'test',
+ };
+ const stateAfterInsert = textBufferReducer(initialState, insertAction);
+ expect(stateAfterInsert.lines).toEqual(['test']);
+ expect(stateAfterInsert.undoStack.length).toBe(1);
+
+ // 2. Undo
+ const undoAction: TextBufferAction = { type: 'undo' };
+ const stateAfterUndo = textBufferReducer(stateAfterInsert, undoAction);
+ expect(stateAfterUndo.lines).toEqual(['']);
+ expect(stateAfterUndo.undoStack.length).toBe(0);
+ expect(stateAfterUndo.redoStack.length).toBe(1);
+
+ // 3. Redo
+ const redoAction: TextBufferAction = { type: 'redo' };
+ const stateAfterRedo = textBufferReducer(stateAfterUndo, redoAction);
+ expect(stateAfterRedo.lines).toEqual(['test']);
+ expect(stateAfterRedo.undoStack.length).toBe(1);
+ expect(stateAfterRedo.redoStack.length).toBe(0);
+ });
+ });
+
+ describe('create_undo_snapshot action', () => {
+ it('should create a snapshot without changing state', () => {
+ const stateWithText: TextBufferState = {
+ ...initialState,
+ lines: ['hello'],
+ cursorRow: 0,
+ cursorCol: 5,
+ };
+ const action: TextBufferAction = { type: 'create_undo_snapshot' };
+ const state = textBufferReducer(stateWithText, action);
+
+ expect(state.lines).toEqual(['hello']);
+ expect(state.cursorRow).toBe(0);
+ expect(state.cursorCol).toBe(5);
+ expect(state.undoStack.length).toBe(1);
+ expect(state.undoStack[0].lines).toEqual(['hello']);
+ expect(state.undoStack[0].cursorRow).toBe(0);
+ expect(state.undoStack[0].cursorCol).toBe(5);
+ });
+ });
+});
+
+// Helper to get the state from the hook
+const getBufferState = (result: { current: TextBuffer }) => ({
+ text: result.current.text,
+ lines: [...result.current.lines], // Clone for safety
+ cursor: [...result.current.cursor] as [number, number],
+ allVisualLines: [...result.current.allVisualLines],
+ viewportVisualLines: [...result.current.viewportVisualLines],
+ visualCursor: [...result.current.visualCursor] as [number, number],
+ visualScrollRow: result.current.visualScrollRow,
+ preferredCol: result.current.preferredCol,
+});
+
+describe('useTextBuffer', () => {
+ let viewport: Viewport;
+
+ beforeEach(() => {
+ viewport = { width: 10, height: 3 }; // Default viewport for tests
+ });
+
+ describe('Initialization', () => {
+ it('should initialize with empty text and cursor at (0,0) by default', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ const state = getBufferState(result);
+ expect(state.text).toBe('');
+ expect(state.lines).toEqual(['']);
+ expect(state.cursor).toEqual([0, 0]);
+ expect(state.allVisualLines).toEqual(['']);
+ expect(state.viewportVisualLines).toEqual(['']);
+ expect(state.visualCursor).toEqual([0, 0]);
+ expect(state.visualScrollRow).toBe(0);
+ });
+
+ it('should initialize with provided initialText', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'hello',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ const state = getBufferState(result);
+ expect(state.text).toBe('hello');
+ expect(state.lines).toEqual(['hello']);
+ expect(state.cursor).toEqual([0, 0]); // Default cursor if offset not given
+ expect(state.allVisualLines).toEqual(['hello']);
+ expect(state.viewportVisualLines).toEqual(['hello']);
+ expect(state.visualCursor).toEqual([0, 0]);
+ });
+
+ it('should initialize with initialText and initialCursorOffset', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'hello\nworld',
+ initialCursorOffset: 7, // Should be at 'o' in 'world'
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ const state = getBufferState(result);
+ expect(state.text).toBe('hello\nworld');
+ expect(state.lines).toEqual(['hello', 'world']);
+ expect(state.cursor).toEqual([1, 1]); // Logical cursor at 'o' in "world"
+ expect(state.allVisualLines).toEqual(['hello', 'world']);
+ expect(state.viewportVisualLines).toEqual(['hello', 'world']);
+ expect(state.visualCursor[0]).toBe(1); // On the second visual line
+ expect(state.visualCursor[1]).toBe(1); // At 'o' in "world"
+ });
+
+ it('should wrap visual lines', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'The quick brown fox jumps over the lazy dog.',
+ initialCursorOffset: 2, // After 'ๅฅฝ'
+ viewport: { width: 15, height: 4 },
+ isValidPath: () => false,
+ }),
+ );
+ const state = getBufferState(result);
+ expect(state.allVisualLines).toEqual([
+ 'The quick',
+ 'brown fox',
+ 'jumps over the',
+ 'lazy dog.',
+ ]);
+ });
+
+ it('should wrap visual lines with multiple spaces', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'The quick brown fox jumps over the lazy dog.',
+ viewport: { width: 15, height: 4 },
+ isValidPath: () => false,
+ }),
+ );
+ const state = getBufferState(result);
+ // Including multiple spaces at the end of the lines like this is
+ // consistent with Google docs behavior and makes it intuitive to edit
+ // the spaces as needed.
+ expect(state.allVisualLines).toEqual([
+ 'The quick ',
+ 'brown fox ',
+ 'jumps over the',
+ 'lazy dog.',
+ ]);
+ });
+
+ it('should wrap visual lines even without spaces', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: '123456789012345ABCDEFG', // 4 chars, 12 bytes
+ viewport: { width: 15, height: 2 },
+ isValidPath: () => false,
+ }),
+ );
+ const state = getBufferState(result);
+ // Including multiple spaces at the end of the lines like this is
+ // consistent with Google docs behavior and makes it intuitive to edit
+ // the spaces as needed.
+ expect(state.allVisualLines).toEqual(['123456789012345', 'ABCDEFG']);
+ });
+
+ it('should initialize with multi-byte unicode characters and correct cursor offset', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'ไฝ ๅฅฝไธ็', // 4 chars, 12 bytes
+ initialCursorOffset: 2, // After 'ๅฅฝ'
+ viewport: { width: 5, height: 2 },
+ isValidPath: () => false,
+ }),
+ );
+ const state = getBufferState(result);
+ expect(state.text).toBe('ไฝ ๅฅฝไธ็');
+ expect(state.lines).toEqual(['ไฝ ๅฅฝไธ็']);
+ expect(state.cursor).toEqual([0, 2]);
+ // Visual: "ไฝ ๅฅฝ" (width 4), "ไธ"็" (width 4) with viewport width 5
+ expect(state.allVisualLines).toEqual(['ไฝ ๅฅฝ', 'ไธ็']);
+ expect(state.visualCursor).toEqual([1, 0]);
+ });
+ });
+
+ describe('Basic Editing', () => {
+ it('insert: should insert a character and update cursor', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ act(() => result.current.insert('a'));
+ let state = getBufferState(result);
+ expect(state.text).toBe('a');
+ expect(state.cursor).toEqual([0, 1]);
+ expect(state.visualCursor).toEqual([0, 1]);
+
+ act(() => result.current.insert('b'));
+ state = getBufferState(result);
+ expect(state.text).toBe('ab');
+ expect(state.cursor).toEqual([0, 2]);
+ expect(state.visualCursor).toEqual([0, 2]);
+ });
+
+ it('insert: should insert text in the middle of a line', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'abc',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('right'));
+ act(() => result.current.insert('-NEW-'));
+ const state = getBufferState(result);
+ expect(state.text).toBe('a-NEW-bc');
+ expect(state.cursor).toEqual([0, 6]);
+ });
+
+ it('newline: should create a new line and move cursor', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'ab',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('end')); // cursor at [0,2]
+ act(() => result.current.newline());
+ const state = getBufferState(result);
+ expect(state.text).toBe('ab\n');
+ expect(state.lines).toEqual(['ab', '']);
+ expect(state.cursor).toEqual([1, 0]);
+ expect(state.allVisualLines).toEqual(['ab', '']);
+ expect(state.viewportVisualLines).toEqual(['ab', '']); // viewport height 3
+ expect(state.visualCursor).toEqual([1, 0]); // On the new visual line
+ });
+
+ it('backspace: should delete char to the left or merge lines', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'a\nb',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => {
+ result.current.move('down');
+ });
+ act(() => {
+ result.current.move('end'); // cursor to [1,1] (end of 'b')
+ });
+ act(() => result.current.backspace()); // delete 'b'
+ let state = getBufferState(result);
+ expect(state.text).toBe('a\n');
+ expect(state.cursor).toEqual([1, 0]);
+
+ act(() => result.current.backspace()); // merge lines
+ state = getBufferState(result);
+ expect(state.text).toBe('a');
+ expect(state.cursor).toEqual([0, 1]); // cursor after 'a'
+ expect(state.allVisualLines).toEqual(['a']);
+ expect(state.viewportVisualLines).toEqual(['a']);
+ expect(state.visualCursor).toEqual([0, 1]);
+ });
+
+ it('del: should delete char to the right or merge lines', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'a\nb',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ // cursor at [0,0]
+ act(() => result.current.del()); // delete 'a'
+ let state = getBufferState(result);
+ expect(state.text).toBe('\nb');
+ expect(state.cursor).toEqual([0, 0]);
+
+ act(() => result.current.del()); // merge lines (deletes newline)
+ state = getBufferState(result);
+ expect(state.text).toBe('b');
+ expect(state.cursor).toEqual([0, 0]);
+ expect(state.allVisualLines).toEqual(['b']);
+ expect(state.viewportVisualLines).toEqual(['b']);
+ expect(state.visualCursor).toEqual([0, 0]);
+ });
+ });
+
+ describe('Drag and Drop File Paths', () => {
+ it('should prepend @ to a valid file path on insert', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => true }),
+ );
+ const filePath = '/path/to/a/valid/file.txt';
+ act(() => result.current.insert(filePath));
+ expect(getBufferState(result).text).toBe(`@${filePath}`);
+ });
+
+ it('should not prepend @ to an invalid file path on insert', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ const notAPath = 'this is just some long text';
+ act(() => result.current.insert(notAPath));
+ expect(getBufferState(result).text).toBe(notAPath);
+ });
+
+ it('should handle quoted paths', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => true }),
+ );
+ const filePath = "'/path/to/a/valid/file.txt'";
+ act(() => result.current.insert(filePath));
+ expect(getBufferState(result).text).toBe(`@/path/to/a/valid/file.txt`);
+ });
+
+ it('should not prepend @ to short text that is not a path', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => true }),
+ );
+ const shortText = 'ab';
+ act(() => result.current.insert(shortText));
+ expect(getBufferState(result).text).toBe(shortText);
+ });
+ });
+
+ describe('Shell Mode Behavior', () => {
+ it('should not prepend @ to valid file paths when shellModeActive is true', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ viewport,
+ isValidPath: () => true,
+ shellModeActive: true,
+ }),
+ );
+ const filePath = '/path/to/a/valid/file.txt';
+ act(() => result.current.insert(filePath));
+ expect(getBufferState(result).text).toBe(filePath); // No @ prefix
+ });
+
+ it('should not prepend @ to quoted paths when shellModeActive is true', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ viewport,
+ isValidPath: () => true,
+ shellModeActive: true,
+ }),
+ );
+ const quotedFilePath = "'/path/to/a/valid/file.txt'";
+ act(() => result.current.insert(quotedFilePath));
+ expect(getBufferState(result).text).toBe(quotedFilePath); // No @ prefix, keeps quotes
+ });
+
+ it('should behave normally with invalid paths when shellModeActive is true', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ viewport,
+ isValidPath: () => false,
+ shellModeActive: true,
+ }),
+ );
+ const notAPath = 'this is just some text';
+ act(() => result.current.insert(notAPath));
+ expect(getBufferState(result).text).toBe(notAPath);
+ });
+
+ it('should behave normally with short text when shellModeActive is true', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ viewport,
+ isValidPath: () => true,
+ shellModeActive: true,
+ }),
+ );
+ const shortText = 'ls';
+ act(() => result.current.insert(shortText));
+ expect(getBufferState(result).text).toBe(shortText); // No @ prefix for short text
+ });
+ });
+
+ describe('Cursor Movement', () => {
+ it('move: left/right should work within and across visual lines (due to wrapping)', () => {
+ // Text: "long line1next line2" (20 chars)
+ // Viewport width 5. Word wrapping should produce:
+ // "long " (5)
+ // "line1" (5)
+ // "next " (5)
+ // "line2" (5)
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'long line1next line2', // Corrected: was 'long line1next line2'
+ viewport: { width: 5, height: 4 },
+ isValidPath: () => false,
+ }),
+ );
+ // Initial cursor [0,0] logical, visual [0,0] ("l" of "long ")
+
+ act(() => result.current.move('right')); // visual [0,1] ("o")
+ expect(getBufferState(result).visualCursor).toEqual([0, 1]);
+ act(() => result.current.move('right')); // visual [0,2] ("n")
+ act(() => result.current.move('right')); // visual [0,3] ("g")
+ act(() => result.current.move('right')); // visual [0,4] (" ")
+ expect(getBufferState(result).visualCursor).toEqual([0, 4]);
+
+ act(() => result.current.move('right')); // visual [1,0] ("l" of "line1")
+ expect(getBufferState(result).visualCursor).toEqual([1, 0]);
+ expect(getBufferState(result).cursor).toEqual([0, 5]); // logical cursor
+
+ act(() => result.current.move('left')); // visual [0,4] (" " of "long ")
+ expect(getBufferState(result).visualCursor).toEqual([0, 4]);
+ expect(getBufferState(result).cursor).toEqual([0, 4]); // logical cursor
+ });
+
+ it('move: up/down should preserve preferred visual column', () => {
+ const text = 'abcde\nxy\n12345';
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: text,
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ expect(result.current.allVisualLines).toEqual(['abcde', 'xy', '12345']);
+ // Place cursor at the end of "abcde" -> logical [0,5]
+ act(() => {
+ result.current.move('home'); // to [0,0]
+ });
+ for (let i = 0; i < 5; i++) {
+ act(() => {
+ result.current.move('right'); // to [0,5]
+ });
+ }
+ expect(getBufferState(result).cursor).toEqual([0, 5]);
+ expect(getBufferState(result).visualCursor).toEqual([0, 5]);
+
+ // Set preferredCol by moving up then down to the same spot, then test.
+ act(() => {
+ result.current.move('down'); // to xy, logical [1,2], visual [1,2], preferredCol should be 5
+ });
+ let state = getBufferState(result);
+ expect(state.cursor).toEqual([1, 2]); // Logical cursor at end of 'xy'
+ expect(state.visualCursor).toEqual([1, 2]); // Visual cursor at end of 'xy'
+ expect(state.preferredCol).toBe(5);
+
+ act(() => result.current.move('down')); // to '12345', preferredCol=5.
+ state = getBufferState(result);
+ expect(state.cursor).toEqual([2, 5]); // Logical cursor at end of '12345'
+ expect(state.visualCursor).toEqual([2, 5]); // Visual cursor at end of '12345'
+ expect(state.preferredCol).toBe(5); // Preferred col is maintained
+
+ act(() => result.current.move('left')); // preferredCol should reset
+ state = getBufferState(result);
+ expect(state.preferredCol).toBe(null);
+ });
+
+ it('move: home/end should go to visual line start/end', () => {
+ const initialText = 'line one\nsecond line';
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText,
+ viewport: { width: 5, height: 5 },
+ isValidPath: () => false,
+ }),
+ );
+ expect(result.current.allVisualLines).toEqual([
+ 'line',
+ 'one',
+ 'secon',
+ 'd',
+ 'line',
+ ]);
+ // Initial cursor [0,0] (start of "line")
+ act(() => result.current.move('down')); // visual cursor from [0,0] to [1,0] ("o" of "one")
+ act(() => result.current.move('right')); // visual cursor to [1,1] ("n" of "one")
+ expect(getBufferState(result).visualCursor).toEqual([1, 1]);
+
+ act(() => result.current.move('home')); // visual cursor to [1,0] (start of "one")
+ expect(getBufferState(result).visualCursor).toEqual([1, 0]);
+
+ act(() => result.current.move('end')); // visual cursor to [1,3] (end of "one")
+ expect(getBufferState(result).visualCursor).toEqual([1, 3]); // "one" is 3 chars
+ });
+ });
+
+ describe('Visual Layout & Viewport', () => {
+ it('should wrap long lines correctly into visualLines', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'This is a very long line of text.', // 33 chars
+ viewport: { width: 10, height: 5 },
+ isValidPath: () => false,
+ }),
+ );
+ const state = getBufferState(result);
+ // Expected visual lines with word wrapping (viewport width 10):
+ // "This is a"
+ // "very long"
+ // "line of"
+ // "text."
+ expect(state.allVisualLines.length).toBe(4);
+ expect(state.allVisualLines[0]).toBe('This is a');
+ expect(state.allVisualLines[1]).toBe('very long');
+ expect(state.allVisualLines[2]).toBe('line of');
+ expect(state.allVisualLines[3]).toBe('text.');
+ });
+
+ it('should update visualScrollRow when visualCursor moves out of viewport', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'l1\nl2\nl3\nl4\nl5',
+ viewport: { width: 5, height: 3 }, // Can show 3 visual lines
+ isValidPath: () => false,
+ }),
+ );
+ // Initial: l1, l2, l3 visible. visualScrollRow = 0. visualCursor = [0,0]
+ expect(getBufferState(result).visualScrollRow).toBe(0);
+ expect(getBufferState(result).allVisualLines).toEqual([
+ 'l1',
+ 'l2',
+ 'l3',
+ 'l4',
+ 'l5',
+ ]);
+ expect(getBufferState(result).viewportVisualLines).toEqual([
+ 'l1',
+ 'l2',
+ 'l3',
+ ]);
+
+ act(() => result.current.move('down')); // vc=[1,0]
+ act(() => result.current.move('down')); // vc=[2,0] (l3)
+ expect(getBufferState(result).visualScrollRow).toBe(0);
+
+ act(() => result.current.move('down')); // vc=[3,0] (l4) - scroll should happen
+ // Now: l2, l3, l4 visible. visualScrollRow = 1.
+ let state = getBufferState(result);
+ expect(state.visualScrollRow).toBe(1);
+ expect(state.allVisualLines).toEqual(['l1', 'l2', 'l3', 'l4', 'l5']);
+ expect(state.viewportVisualLines).toEqual(['l2', 'l3', 'l4']);
+ expect(state.visualCursor).toEqual([3, 0]);
+
+ act(() => result.current.move('up')); // vc=[2,0] (l3)
+ act(() => result.current.move('up')); // vc=[1,0] (l2)
+ expect(getBufferState(result).visualScrollRow).toBe(1);
+
+ act(() => result.current.move('up')); // vc=[0,0] (l1) - scroll up
+ // Now: l1, l2, l3 visible. visualScrollRow = 0
+ state = getBufferState(result); // Assign to the existing `state` variable
+ expect(state.visualScrollRow).toBe(0);
+ expect(state.allVisualLines).toEqual(['l1', 'l2', 'l3', 'l4', 'l5']);
+ expect(state.viewportVisualLines).toEqual(['l1', 'l2', 'l3']);
+ expect(state.visualCursor).toEqual([0, 0]);
+ });
+ });
+
+ describe('Undo/Redo', () => {
+ it('should undo and redo an insert operation', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ act(() => result.current.insert('a'));
+ expect(getBufferState(result).text).toBe('a');
+
+ act(() => result.current.undo());
+ expect(getBufferState(result).text).toBe('');
+ expect(getBufferState(result).cursor).toEqual([0, 0]);
+
+ act(() => result.current.redo());
+ expect(getBufferState(result).text).toBe('a');
+ expect(getBufferState(result).cursor).toEqual([0, 1]);
+ });
+
+ it('should undo and redo a newline operation', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'test',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('end'));
+ act(() => result.current.newline());
+ expect(getBufferState(result).text).toBe('test\n');
+
+ act(() => result.current.undo());
+ expect(getBufferState(result).text).toBe('test');
+ expect(getBufferState(result).cursor).toEqual([0, 4]);
+
+ act(() => result.current.redo());
+ expect(getBufferState(result).text).toBe('test\n');
+ expect(getBufferState(result).cursor).toEqual([1, 0]);
+ });
+ });
+
+ describe('Unicode Handling', () => {
+ it('insert: should correctly handle multi-byte unicode characters', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ act(() => result.current.insert('ไฝ ๅฅฝ'));
+ const state = getBufferState(result);
+ expect(state.text).toBe('ไฝ ๅฅฝ');
+ expect(state.cursor).toEqual([0, 2]); // Cursor is 2 (char count)
+ expect(state.visualCursor).toEqual([0, 2]);
+ });
+
+ it('backspace: should correctly delete multi-byte unicode characters', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'ไฝ ๅฅฝ',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('end')); // cursor at [0,2]
+ act(() => result.current.backspace()); // delete 'ๅฅฝ'
+ let state = getBufferState(result);
+ expect(state.text).toBe('ไฝ ');
+ expect(state.cursor).toEqual([0, 1]);
+
+ act(() => result.current.backspace()); // delete 'ไฝ '
+ state = getBufferState(result);
+ expect(state.text).toBe('');
+ expect(state.cursor).toEqual([0, 0]);
+ });
+
+ it('move: left/right should treat multi-byte chars as single units for visual cursor', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: '๐ถ๐ฑ',
+ viewport: { width: 5, height: 1 },
+ isValidPath: () => false,
+ }),
+ );
+ // Initial: visualCursor [0,0]
+ act(() => result.current.move('right')); // visualCursor [0,1] (after ๐ถ)
+ let state = getBufferState(result);
+ expect(state.cursor).toEqual([0, 1]);
+ expect(state.visualCursor).toEqual([0, 1]);
+
+ act(() => result.current.move('right')); // visualCursor [0,2] (after ๐ฑ)
+ state = getBufferState(result);
+ expect(state.cursor).toEqual([0, 2]);
+ expect(state.visualCursor).toEqual([0, 2]);
+
+ act(() => result.current.move('left')); // visualCursor [0,1] (before ๐ฑ / after ๐ถ)
+ state = getBufferState(result);
+ expect(state.cursor).toEqual([0, 1]);
+ expect(state.visualCursor).toEqual([0, 1]);
+ });
+ });
+
+ describe('handleInput', () => {
+ it('should insert printable characters', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ act(() =>
+ result.current.handleInput({
+ name: 'h',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: 'h',
+ }),
+ );
+ act(() =>
+ result.current.handleInput({
+ name: 'i',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: 'i',
+ }),
+ );
+ expect(getBufferState(result).text).toBe('hi');
+ });
+
+ it('should handle "Enter" key as newline', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ act(() =>
+ result.current.handleInput({
+ name: 'return',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: '\r',
+ }),
+ );
+ expect(getBufferState(result).lines).toEqual(['', '']);
+ });
+
+ it('should handle "Backspace" key', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'a',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('end'));
+ act(() =>
+ result.current.handleInput({
+ name: 'backspace',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: '\x7f',
+ }),
+ );
+ expect(getBufferState(result).text).toBe('');
+ });
+
+ it('should handle multiple delete characters in one input', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'abcde',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('end')); // cursor at the end
+ expect(getBufferState(result).cursor).toEqual([0, 5]);
+
+ act(() => {
+ result.current.handleInput({
+ name: 'backspace',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ sequence: '\x7f',
+ });
+ result.current.handleInput({
+ name: 'backspace',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ sequence: '\x7f',
+ });
+ result.current.handleInput({
+ name: 'backspace',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ sequence: '\x7f',
+ });
+ });
+ expect(getBufferState(result).text).toBe('ab');
+ expect(getBufferState(result).cursor).toEqual([0, 2]);
+ });
+
+ it('should handle inserts that contain delete characters ', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'abcde',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('end')); // cursor at the end
+ expect(getBufferState(result).cursor).toEqual([0, 5]);
+
+ act(() => {
+ result.current.insert('\x7f\x7f\x7f');
+ });
+ expect(getBufferState(result).text).toBe('ab');
+ expect(getBufferState(result).cursor).toEqual([0, 2]);
+ });
+
+ it('should handle inserts with a mix of regular and delete characters ', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'abcde',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('end')); // cursor at the end
+ expect(getBufferState(result).cursor).toEqual([0, 5]);
+
+ act(() => {
+ result.current.insert('\x7fI\x7f\x7fNEW');
+ });
+ expect(getBufferState(result).text).toBe('abcNEW');
+ expect(getBufferState(result).cursor).toEqual([0, 6]);
+ });
+
+ it('should handle arrow keys for movement', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'ab',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.move('end')); // cursor [0,2]
+ act(() =>
+ result.current.handleInput({
+ name: 'left',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: '\x1b[D',
+ }),
+ ); // cursor [0,1]
+ expect(getBufferState(result).cursor).toEqual([0, 1]);
+ act(() =>
+ result.current.handleInput({
+ name: 'right',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: '\x1b[C',
+ }),
+ ); // cursor [0,2]
+ expect(getBufferState(result).cursor).toEqual([0, 2]);
+ });
+
+ it('should strip ANSI escape codes when pasting text', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ const textWithAnsi = '\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m';
+ // Simulate pasting by calling handleInput with a string longer than 1 char
+ act(() =>
+ result.current.handleInput({
+ name: '',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: textWithAnsi,
+ }),
+ );
+ expect(getBufferState(result).text).toBe('Hello World');
+ });
+
+ it('should handle VSCode terminal Shift+Enter as newline', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ act(() =>
+ result.current.handleInput({
+ name: 'return',
+ ctrl: false,
+ meta: false,
+ shift: true,
+ paste: false,
+ sequence: '\r',
+ }),
+ ); // Simulates Shift+Enter in VSCode terminal
+ expect(getBufferState(result).lines).toEqual(['', '']);
+ });
+
+ it('should correctly handle repeated pasting of long text', () => {
+ const longText = `not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
+
+Why do we use it?
+It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
+
+Where does it come from?
+Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lore
+`;
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+
+ // Simulate pasting the long text multiple times
+ act(() => {
+ result.current.insert(longText);
+ result.current.insert(longText);
+ result.current.insert(longText);
+ });
+
+ const state = getBufferState(result);
+ // Check that the text is the result of three concatenations.
+ expect(state.lines).toStrictEqual(
+ (longText + longText + longText).split('\n'),
+ );
+ const expectedCursorPos = offsetToLogicalPos(
+ state.text,
+ state.text.length,
+ );
+ expect(state.cursor).toEqual(expectedCursorPos);
+ });
+ });
+
+ // More tests would be needed for:
+ // - setText, replaceRange
+ // - deleteWordLeft, deleteWordRight
+ // - More complex undo/redo scenarios
+ // - Selection and clipboard (copy/paste) - might need clipboard API mocks or internal state check
+ // - openInExternalEditor (heavy mocking of fs, child_process, os)
+ // - All edge cases for visual scrolling and wrapping with different viewport sizes and text content.
+
+ describe('replaceRange', () => {
+ it('should replace a single-line range with single-line text', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: '@pac',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.replaceRange(0, 1, 0, 4, 'packages'));
+ const state = getBufferState(result);
+ expect(state.text).toBe('@packages');
+ expect(state.cursor).toEqual([0, 9]); // cursor after 'typescript'
+ });
+
+ it('should replace a multi-line range with single-line text', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'hello\nworld\nagain',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.replaceRange(0, 2, 1, 3, ' new ')); // replace 'llo\nwor' with ' new '
+ const state = getBufferState(result);
+ expect(state.text).toBe('he new ld\nagain');
+ expect(state.cursor).toEqual([0, 7]); // cursor after ' new '
+ });
+
+ it('should delete a range when replacing with an empty string', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'hello world',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.replaceRange(0, 5, 0, 11, '')); // delete ' world'
+ const state = getBufferState(result);
+ expect(state.text).toBe('hello');
+ expect(state.cursor).toEqual([0, 5]);
+ });
+
+ it('should handle replacing at the beginning of the text', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'world',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.replaceRange(0, 0, 0, 0, 'hello '));
+ const state = getBufferState(result);
+ expect(state.text).toBe('hello world');
+ expect(state.cursor).toEqual([0, 6]);
+ });
+
+ it('should handle replacing at the end of the text', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'hello',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.replaceRange(0, 5, 0, 5, ' world'));
+ const state = getBufferState(result);
+ expect(state.text).toBe('hello world');
+ expect(state.cursor).toEqual([0, 11]);
+ });
+
+ it('should handle replacing the entire buffer content', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'old text',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.replaceRange(0, 0, 0, 8, 'new text'));
+ const state = getBufferState(result);
+ expect(state.text).toBe('new text');
+ expect(state.cursor).toEqual([0, 8]);
+ });
+
+ it('should correctly replace with unicode characters', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'hello *** world',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.replaceRange(0, 6, 0, 9, 'ไฝ ๅฅฝ'));
+ const state = getBufferState(result);
+ expect(state.text).toBe('hello ไฝ ๅฅฝ world');
+ expect(state.cursor).toEqual([0, 8]); // after 'ไฝ ๅฅฝ'
+ });
+
+ it('should handle invalid range by returning false and not changing text', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'test',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => {
+ result.current.replaceRange(0, 5, 0, 3, 'fail'); // startCol > endCol in same line
+ });
+
+ expect(getBufferState(result).text).toBe('test');
+
+ act(() => {
+ result.current.replaceRange(1, 0, 0, 0, 'fail'); // startRow > endRow
+ });
+ expect(getBufferState(result).text).toBe('test');
+ });
+
+ it('replaceRange: multiple lines with a single character', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({
+ initialText: 'first\nsecond\nthird',
+ viewport,
+ isValidPath: () => false,
+ }),
+ );
+ act(() => result.current.replaceRange(0, 2, 2, 3, 'X')); // Replace 'rst\nsecond\nthi'
+ const state = getBufferState(result);
+ expect(state.text).toBe('fiXrd');
+ expect(state.cursor).toEqual([0, 3]); // After 'X'
+ });
+ });
+
+ describe('Input Sanitization', () => {
+ it('should strip ANSI escape codes from input', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ const textWithAnsi = '\x1B[31mHello\x1B[0m';
+ act(() =>
+ result.current.handleInput({
+ name: '',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: textWithAnsi,
+ }),
+ );
+ expect(getBufferState(result).text).toBe('Hello');
+ });
+
+ it('should strip control characters from input', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ const textWithControlChars = 'H\x07e\x08l\x0Bl\x0Co'; // BELL, BACKSPACE, VT, FF
+ act(() =>
+ result.current.handleInput({
+ name: '',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: textWithControlChars,
+ }),
+ );
+ expect(getBufferState(result).text).toBe('Hello');
+ });
+
+ it('should strip mixed ANSI and control characters from input', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ const textWithMixed = '\u001B[4mH\u001B[0mello';
+ act(() =>
+ result.current.handleInput({
+ name: '',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: textWithMixed,
+ }),
+ );
+ expect(getBufferState(result).text).toBe('Hello');
+ });
+
+ it('should not strip standard characters or newlines', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ const validText = 'Hello World\nThis is a test.';
+ act(() =>
+ result.current.handleInput({
+ name: '',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: validText,
+ }),
+ );
+ expect(getBufferState(result).text).toBe(validText);
+ });
+
+ it('should sanitize pasted text via handleInput', () => {
+ const { result } = renderHook(() =>
+ useTextBuffer({ viewport, isValidPath: () => false }),
+ );
+ const pastedText = '\u001B[4mPasted\u001B[4m Text';
+ act(() =>
+ result.current.handleInput({
+ name: '',
+ ctrl: false,
+ meta: false,
+ shift: false,
+ paste: false,
+ sequence: pastedText,
+ }),
+ );
+ expect(getBufferState(result).text).toBe('Pasted Text');
+ });
+ });
+});
+
+describe('offsetToLogicalPos', () => {
+ it('should return [0,0] for offset 0', () => {
+ expect(offsetToLogicalPos('any text', 0)).toEqual([0, 0]);
+ });
+
+ it('should handle single line text', () => {
+ const text = 'hello';
+ expect(offsetToLogicalPos(text, 0)).toEqual([0, 0]); // Start
+ expect(offsetToLogicalPos(text, 2)).toEqual([0, 2]); // Middle 'l'
+ expect(offsetToLogicalPos(text, 5)).toEqual([0, 5]); // End
+ expect(offsetToLogicalPos(text, 10)).toEqual([0, 5]); // Beyond end
+ });
+
+ it('should handle multi-line text', () => {
+ const text = 'hello\nworld\n123';
+ // "hello" (5) + \n (1) + "world" (5) + \n (1) + "123" (3)
+ // h e l l o \n w o r l d \n 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
+ // Line 0: "hello" (length 5)
+ expect(offsetToLogicalPos(text, 0)).toEqual([0, 0]); // Start of 'hello'
+ expect(offsetToLogicalPos(text, 3)).toEqual([0, 3]); // 'l' in 'hello'
+ expect(offsetToLogicalPos(text, 5)).toEqual([0, 5]); // End of 'hello' (before \n)
+
+ // Line 1: "world" (length 5)
+ expect(offsetToLogicalPos(text, 6)).toEqual([1, 0]); // Start of 'world' (after \n)
+ expect(offsetToLogicalPos(text, 8)).toEqual([1, 2]); // 'r' in 'world'
+ expect(offsetToLogicalPos(text, 11)).toEqual([1, 5]); // End of 'world' (before \n)
+
+ // Line 2: "123" (length 3)
+ expect(offsetToLogicalPos(text, 12)).toEqual([2, 0]); // Start of '123' (after \n)
+ expect(offsetToLogicalPos(text, 13)).toEqual([2, 1]); // '2' in '123'
+ expect(offsetToLogicalPos(text, 15)).toEqual([2, 3]); // End of '123'
+ expect(offsetToLogicalPos(text, 20)).toEqual([2, 3]); // Beyond end of text
+ });
+
+ it('should handle empty lines', () => {
+ const text = 'a\n\nc'; // "a" (1) + \n (1) + "" (0) + \n (1) + "c" (1)
+ expect(offsetToLogicalPos(text, 0)).toEqual([0, 0]); // 'a'
+ expect(offsetToLogicalPos(text, 1)).toEqual([0, 1]); // End of 'a'
+ expect(offsetToLogicalPos(text, 2)).toEqual([1, 0]); // Start of empty line
+ expect(offsetToLogicalPos(text, 3)).toEqual([2, 0]); // Start of 'c'
+ expect(offsetToLogicalPos(text, 4)).toEqual([2, 1]); // End of 'c'
+ });
+
+ it('should handle text ending with a newline', () => {
+ const text = 'hello\n'; // "hello" (5) + \n (1)
+ expect(offsetToLogicalPos(text, 5)).toEqual([0, 5]); // End of 'hello'
+ expect(offsetToLogicalPos(text, 6)).toEqual([1, 0]); // Position on the new empty line after
+
+ expect(offsetToLogicalPos(text, 7)).toEqual([1, 0]); // Still on the new empty line
+ });
+
+ it('should handle text starting with a newline', () => {
+ const text = '\nhello'; // "" (0) + \n (1) + "hello" (5)
+ expect(offsetToLogicalPos(text, 0)).toEqual([0, 0]); // Start of first empty line
+ expect(offsetToLogicalPos(text, 1)).toEqual([1, 0]); // Start of 'hello'
+ expect(offsetToLogicalPos(text, 3)).toEqual([1, 2]); // 'l' in 'hello'
+ });
+
+ it('should handle empty string input', () => {
+ expect(offsetToLogicalPos('', 0)).toEqual([0, 0]);
+ expect(offsetToLogicalPos('', 5)).toEqual([0, 0]);
+ });
+
+ it('should handle multi-byte unicode characters correctly', () => {
+ const text = 'ไฝ ๅฅฝ\nไธ็'; // "ไฝ ๅฅฝ" (2 chars) + \n (1) + "ไธ็" (2 chars)
+ // Total "code points" for offset calculation: 2 + 1 + 2 = 5
+ expect(offsetToLogicalPos(text, 0)).toEqual([0, 0]); // Start of 'ไฝ ๅฅฝ'
+ expect(offsetToLogicalPos(text, 1)).toEqual([0, 1]); // After 'ไฝ ', before 'ๅฅฝ'
+ expect(offsetToLogicalPos(text, 2)).toEqual([0, 2]); // End of 'ไฝ ๅฅฝ'
+ expect(offsetToLogicalPos(text, 3)).toEqual([1, 0]); // Start of 'ไธ็'
+ expect(offsetToLogicalPos(text, 4)).toEqual([1, 1]); // After 'ไธ', before '็'
+ expect(offsetToLogicalPos(text, 5)).toEqual([1, 2]); // End of 'ไธ็'
+ expect(offsetToLogicalPos(text, 6)).toEqual([1, 2]); // Beyond end
+ });
+
+ it('should handle offset exactly at newline character', () => {
+ const text = 'abc\ndef';
+ // a b c \n d e f
+ // 0 1 2 3 4 5 6
+ expect(offsetToLogicalPos(text, 3)).toEqual([0, 3]); // End of 'abc'
+ // The next character is the newline, so an offset of 4 means the start of the next line.
+ expect(offsetToLogicalPos(text, 4)).toEqual([1, 0]); // Start of 'def'
+ });
+
+ it('should handle offset in the middle of a multi-byte character (should place at start of that char)', () => {
+ // This scenario is tricky as "offset" is usually character-based.
+ // Assuming cpLen and related logic handles this by treating multi-byte as one unit.
+ // The current implementation of offsetToLogicalPos uses cpLen, so it should be code-point aware.
+ const text = '๐ถ๐ฑ'; // 2 code points
+ expect(offsetToLogicalPos(text, 0)).toEqual([0, 0]);
+ expect(offsetToLogicalPos(text, 1)).toEqual([0, 1]); // After ๐ถ
+ expect(offsetToLogicalPos(text, 2)).toEqual([0, 2]); // After ๐ฑ
+ });
+});
diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts
new file mode 100644
index 00000000..e0bb10cb
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/text-buffer.ts
@@ -0,0 +1,1389 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import stripAnsi from 'strip-ansi';
+import { spawnSync } from 'child_process';
+import fs from 'fs';
+import os from 'os';
+import pathMod from 'path';
+import { useState, useCallback, useEffect, useMemo, useReducer } from 'react';
+import stringWidth from 'string-width';
+import { unescapePath } from '@qwen/qwen-code-core';
+import { toCodePoints, cpLen, cpSlice } from '../../utils/textUtils.js';
+
+export type Direction =
+ | 'left'
+ | 'right'
+ | 'up'
+ | 'down'
+ | 'wordLeft'
+ | 'wordRight'
+ | 'home'
+ | 'end';
+
+// Simple helper for wordโwise ops.
+function isWordChar(ch: string | undefined): boolean {
+ if (ch === undefined) {
+ return false;
+ }
+ return !/[\s,.;!?]/.test(ch);
+}
+
+/**
+ * Strip characters that can break terminal rendering.
+ *
+ * Strip ANSI escape codes and control characters except for line breaks.
+ * Control characters such as delete break terminal UI rendering.
+ */
+function stripUnsafeCharacters(str: string): string {
+ const stripped = stripAnsi(str);
+ return toCodePoints(stripAnsi(stripped))
+ .filter((char) => {
+ if (char.length > 1) return false;
+ const code = char.codePointAt(0);
+ if (code === undefined) {
+ return false;
+ }
+ const isUnsafe =
+ code === 127 || (code <= 31 && code !== 13 && code !== 10);
+ return !isUnsafe;
+ })
+ .join('');
+}
+
+export interface Viewport {
+ height: number;
+ width: number;
+}
+
+function clamp(v: number, min: number, max: number): number {
+ return v < min ? min : v > max ? max : v;
+}
+
+/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+interface UseTextBufferProps {
+ initialText?: string;
+ initialCursorOffset?: number;
+ viewport: Viewport; // Viewport dimensions needed for scrolling
+ stdin?: NodeJS.ReadStream | null; // For external editor
+ setRawMode?: (mode: boolean) => void; // For external editor
+ onChange?: (text: string) => void; // Callback for when text changes
+ isValidPath: (path: string) => boolean;
+ shellModeActive?: boolean; // Whether the text buffer is in shell mode
+}
+
+interface UndoHistoryEntry {
+ lines: string[];
+ cursorRow: number;
+ cursorCol: number;
+}
+
+function calculateInitialCursorPosition(
+ initialLines: string[],
+ offset: number,
+): [number, number] {
+ let remainingChars = offset;
+ let row = 0;
+ while (row < initialLines.length) {
+ const lineLength = cpLen(initialLines[row]);
+ // Add 1 for the newline character (except for the last line)
+ const totalCharsInLineAndNewline =
+ lineLength + (row < initialLines.length - 1 ? 1 : 0);
+
+ if (remainingChars <= lineLength) {
+ // Cursor is on this line
+ return [row, remainingChars];
+ }
+ remainingChars -= totalCharsInLineAndNewline;
+ row++;
+ }
+ // Offset is beyond the text, place cursor at the end of the last line
+ if (initialLines.length > 0) {
+ const lastRow = initialLines.length - 1;
+ return [lastRow, cpLen(initialLines[lastRow])];
+ }
+ return [0, 0]; // Default for empty text
+}
+
+export function offsetToLogicalPos(
+ text: string,
+ offset: number,
+): [number, number] {
+ let row = 0;
+ let col = 0;
+ let currentOffset = 0;
+
+ if (offset === 0) return [0, 0];
+
+ const lines = text.split('\n');
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const lineLength = cpLen(line);
+ const lineLengthWithNewline = lineLength + (i < lines.length - 1 ? 1 : 0);
+
+ if (offset <= currentOffset + lineLength) {
+ // Check against lineLength first
+ row = i;
+ col = offset - currentOffset;
+ return [row, col];
+ } else if (offset <= currentOffset + lineLengthWithNewline) {
+ // Check if offset is the newline itself
+ row = i;
+ col = lineLength; // Position cursor at the end of the current line content
+ // If the offset IS the newline, and it's not the last line, advance to next line, col 0
+ if (
+ offset === currentOffset + lineLengthWithNewline &&
+ i < lines.length - 1
+ ) {
+ return [i + 1, 0];
+ }
+ return [row, col]; // Otherwise, it's at the end of the current line content
+ }
+ currentOffset += lineLengthWithNewline;
+ }
+
+ // If offset is beyond the text length, place cursor at the end of the last line
+ // or [0,0] if text is empty
+ if (lines.length > 0) {
+ row = lines.length - 1;
+ col = cpLen(lines[row]);
+ } else {
+ row = 0;
+ col = 0;
+ }
+ return [row, col];
+}
+
+// Helper to calculate visual lines and map cursor positions
+function calculateVisualLayout(
+ logicalLines: string[],
+ logicalCursor: [number, number],
+ viewportWidth: number,
+): {
+ visualLines: string[];
+ visualCursor: [number, number];
+ logicalToVisualMap: Array>; // For each logical line, an array of [visualLineIndex, startColInLogical]
+ visualToLogicalMap: Array<[number, number]>; // For each visual line, its [logicalLineIndex, startColInLogical]
+} {
+ const visualLines: string[] = [];
+ const logicalToVisualMap: Array> = [];
+ const visualToLogicalMap: Array<[number, number]> = [];
+ let currentVisualCursor: [number, number] = [0, 0];
+
+ logicalLines.forEach((logLine, logIndex) => {
+ logicalToVisualMap[logIndex] = [];
+ if (logLine.length === 0) {
+ // Handle empty logical line
+ logicalToVisualMap[logIndex].push([visualLines.length, 0]);
+ visualToLogicalMap.push([logIndex, 0]);
+ visualLines.push('');
+ if (logIndex === logicalCursor[0] && logicalCursor[1] === 0) {
+ currentVisualCursor = [visualLines.length - 1, 0];
+ }
+ } else {
+ // Non-empty logical line
+ let currentPosInLogLine = 0; // Tracks position within the current logical line (code point index)
+ const codePointsInLogLine = toCodePoints(logLine);
+
+ while (currentPosInLogLine < codePointsInLogLine.length) {
+ let currentChunk = '';
+ let currentChunkVisualWidth = 0;
+ let numCodePointsInChunk = 0;
+ let lastWordBreakPoint = -1; // Index in codePointsInLogLine for word break
+ let numCodePointsAtLastWordBreak = 0;
+
+ // Iterate through code points to build the current visual line (chunk)
+ for (let i = currentPosInLogLine; i < codePointsInLogLine.length; i++) {
+ const char = codePointsInLogLine[i];
+ const charVisualWidth = stringWidth(char);
+
+ if (currentChunkVisualWidth + charVisualWidth > viewportWidth) {
+ // Character would exceed viewport width
+ if (
+ lastWordBreakPoint !== -1 &&
+ numCodePointsAtLastWordBreak > 0 &&
+ currentPosInLogLine + numCodePointsAtLastWordBreak < i
+ ) {
+ // We have a valid word break point to use, and it's not the start of the current segment
+ currentChunk = codePointsInLogLine
+ .slice(
+ currentPosInLogLine,
+ currentPosInLogLine + numCodePointsAtLastWordBreak,
+ )
+ .join('');
+ numCodePointsInChunk = numCodePointsAtLastWordBreak;
+ } else {
+ // No word break, or word break is at the start of this potential chunk, or word break leads to empty chunk.
+ // Hard break: take characters up to viewportWidth, or just the current char if it alone is too wide.
+ if (
+ numCodePointsInChunk === 0 &&
+ charVisualWidth > viewportWidth
+ ) {
+ // Single character is wider than viewport, take it anyway
+ currentChunk = char;
+ numCodePointsInChunk = 1;
+ } else if (
+ numCodePointsInChunk === 0 &&
+ charVisualWidth <= viewportWidth
+ ) {
+ // This case should ideally be caught by the next iteration if the char fits.
+ // If it doesn't fit (because currentChunkVisualWidth was already > 0 from a previous char that filled the line),
+ // then numCodePointsInChunk would not be 0.
+ // This branch means the current char *itself* doesn't fit an empty line, which is handled by the above.
+ // If we are here, it means the loop should break and the current chunk (which is empty) is finalized.
+ }
+ }
+ break; // Break from inner loop to finalize this chunk
+ }
+
+ currentChunk += char;
+ currentChunkVisualWidth += charVisualWidth;
+ numCodePointsInChunk++;
+
+ // Check for word break opportunity (space)
+ if (char === ' ') {
+ lastWordBreakPoint = i; // Store code point index of the space
+ // Store the state *before* adding the space, if we decide to break here.
+ numCodePointsAtLastWordBreak = numCodePointsInChunk - 1; // Chars *before* the space
+ }
+ }
+
+ // If the inner loop completed without breaking (i.e., remaining text fits)
+ // or if the loop broke but numCodePointsInChunk is still 0 (e.g. first char too wide for empty line)
+ if (
+ numCodePointsInChunk === 0 &&
+ currentPosInLogLine < codePointsInLogLine.length
+ ) {
+ // This can happen if the very first character considered for a new visual line is wider than the viewport.
+ // In this case, we take that single character.
+ const firstChar = codePointsInLogLine[currentPosInLogLine];
+ currentChunk = firstChar;
+ numCodePointsInChunk = 1; // Ensure we advance
+ }
+
+ // If after everything, numCodePointsInChunk is still 0 but we haven't processed the whole logical line,
+ // it implies an issue, like viewportWidth being 0 or less. Avoid infinite loop.
+ if (
+ numCodePointsInChunk === 0 &&
+ currentPosInLogLine < codePointsInLogLine.length
+ ) {
+ // Force advance by one character to prevent infinite loop if something went wrong
+ currentChunk = codePointsInLogLine[currentPosInLogLine];
+ numCodePointsInChunk = 1;
+ }
+
+ logicalToVisualMap[logIndex].push([
+ visualLines.length,
+ currentPosInLogLine,
+ ]);
+ visualToLogicalMap.push([logIndex, currentPosInLogLine]);
+ visualLines.push(currentChunk);
+
+ // Cursor mapping logic
+ // Note: currentPosInLogLine here is the start of the currentChunk within the logical line.
+ if (logIndex === logicalCursor[0]) {
+ const cursorLogCol = logicalCursor[1]; // This is a code point index
+ if (
+ cursorLogCol >= currentPosInLogLine &&
+ cursorLogCol < currentPosInLogLine + numCodePointsInChunk // Cursor is within this chunk
+ ) {
+ currentVisualCursor = [
+ visualLines.length - 1,
+ cursorLogCol - currentPosInLogLine, // Visual col is also code point index within visual line
+ ];
+ } else if (
+ cursorLogCol === currentPosInLogLine + numCodePointsInChunk &&
+ numCodePointsInChunk > 0
+ ) {
+ // Cursor is exactly at the end of this non-empty chunk
+ currentVisualCursor = [
+ visualLines.length - 1,
+ numCodePointsInChunk,
+ ];
+ }
+ }
+
+ const logicalStartOfThisChunk = currentPosInLogLine;
+ currentPosInLogLine += numCodePointsInChunk;
+
+ // If the chunk processed did not consume the entire logical line,
+ // and the character immediately following the chunk is a space,
+ // advance past this space as it acted as a delimiter for word wrapping.
+ if (
+ logicalStartOfThisChunk + numCodePointsInChunk <
+ codePointsInLogLine.length &&
+ currentPosInLogLine < codePointsInLogLine.length && // Redundant if previous is true, but safe
+ codePointsInLogLine[currentPosInLogLine] === ' '
+ ) {
+ currentPosInLogLine++;
+ }
+ }
+ // After all chunks of a non-empty logical line are processed,
+ // if the cursor is at the very end of this logical line, update visual cursor.
+ if (
+ logIndex === logicalCursor[0] &&
+ logicalCursor[1] === codePointsInLogLine.length // Cursor at end of logical line
+ ) {
+ const lastVisualLineIdx = visualLines.length - 1;
+ if (
+ lastVisualLineIdx >= 0 &&
+ visualLines[lastVisualLineIdx] !== undefined
+ ) {
+ currentVisualCursor = [
+ lastVisualLineIdx,
+ cpLen(visualLines[lastVisualLineIdx]), // Cursor at end of last visual line for this logical line
+ ];
+ }
+ }
+ }
+ });
+
+ // If the entire logical text was empty, ensure there's one empty visual line.
+ if (
+ logicalLines.length === 0 ||
+ (logicalLines.length === 1 && logicalLines[0] === '')
+ ) {
+ if (visualLines.length === 0) {
+ visualLines.push('');
+ if (!logicalToVisualMap[0]) logicalToVisualMap[0] = [];
+ logicalToVisualMap[0].push([0, 0]);
+ visualToLogicalMap.push([0, 0]);
+ }
+ currentVisualCursor = [0, 0];
+ }
+ // Handle cursor at the very end of the text (after all processing)
+ // This case might be covered by the loop end condition now, but kept for safety.
+ else if (
+ logicalCursor[0] === logicalLines.length - 1 &&
+ logicalCursor[1] === cpLen(logicalLines[logicalLines.length - 1]) &&
+ visualLines.length > 0
+ ) {
+ const lastVisLineIdx = visualLines.length - 1;
+ currentVisualCursor = [lastVisLineIdx, cpLen(visualLines[lastVisLineIdx])];
+ }
+
+ return {
+ visualLines,
+ visualCursor: currentVisualCursor,
+ logicalToVisualMap,
+ visualToLogicalMap,
+ };
+}
+
+// --- Start of reducer logic ---
+
+interface TextBufferState {
+ lines: string[];
+ cursorRow: number;
+ cursorCol: number;
+ preferredCol: number | null; // This is visual preferred col
+ undoStack: UndoHistoryEntry[];
+ redoStack: UndoHistoryEntry[];
+ clipboard: string | null;
+ selectionAnchor: [number, number] | null;
+ viewportWidth: number;
+}
+
+const historyLimit = 100;
+
+type TextBufferAction =
+ | { type: 'set_text'; payload: string; pushToUndo?: boolean }
+ | { type: 'insert'; payload: string }
+ | { type: 'backspace' }
+ | {
+ type: 'move';
+ payload: {
+ dir: Direction;
+ };
+ }
+ | { type: 'delete' }
+ | { type: 'delete_word_left' }
+ | { type: 'delete_word_right' }
+ | { type: 'kill_line_right' }
+ | { type: 'kill_line_left' }
+ | { type: 'undo' }
+ | { type: 'redo' }
+ | {
+ type: 'replace_range';
+ payload: {
+ startRow: number;
+ startCol: number;
+ endRow: number;
+ endCol: number;
+ text: string;
+ };
+ }
+ | { type: 'move_to_offset'; payload: { offset: number } }
+ | { type: 'create_undo_snapshot' }
+ | { type: 'set_viewport_width'; payload: number };
+
+export function textBufferReducer(
+ state: TextBufferState,
+ action: TextBufferAction,
+): TextBufferState {
+ const pushUndo = (currentState: TextBufferState): TextBufferState => {
+ const snapshot = {
+ lines: [...currentState.lines],
+ cursorRow: currentState.cursorRow,
+ cursorCol: currentState.cursorCol,
+ };
+ const newStack = [...currentState.undoStack, snapshot];
+ if (newStack.length > historyLimit) {
+ newStack.shift();
+ }
+ return { ...currentState, undoStack: newStack, redoStack: [] };
+ };
+
+ const currentLine = (r: number): string => state.lines[r] ?? '';
+ const currentLineLen = (r: number): number => cpLen(currentLine(r));
+
+ switch (action.type) {
+ case 'set_text': {
+ let nextState = state;
+ if (action.pushToUndo !== false) {
+ nextState = pushUndo(state);
+ }
+ const newContentLines = action.payload
+ .replace(/\r\n?/g, '\n')
+ .split('\n');
+ const lines = newContentLines.length === 0 ? [''] : newContentLines;
+ const lastNewLineIndex = lines.length - 1;
+ return {
+ ...nextState,
+ lines,
+ cursorRow: lastNewLineIndex,
+ cursorCol: cpLen(lines[lastNewLineIndex] ?? ''),
+ preferredCol: null,
+ };
+ }
+
+ case 'insert': {
+ const nextState = pushUndo(state);
+ const newLines = [...nextState.lines];
+ let newCursorRow = nextState.cursorRow;
+ let newCursorCol = nextState.cursorCol;
+
+ const currentLine = (r: number) => newLines[r] ?? '';
+
+ const str = stripUnsafeCharacters(
+ action.payload.replace(/\r\n/g, '\n').replace(/\r/g, '\n'),
+ );
+ const parts = str.split('\n');
+ const lineContent = currentLine(newCursorRow);
+ const before = cpSlice(lineContent, 0, newCursorCol);
+ const after = cpSlice(lineContent, newCursorCol);
+
+ if (parts.length > 1) {
+ newLines[newCursorRow] = before + parts[0];
+ const remainingParts = parts.slice(1);
+ const lastPartOriginal = remainingParts.pop() ?? '';
+ newLines.splice(newCursorRow + 1, 0, ...remainingParts);
+ newLines.splice(
+ newCursorRow + parts.length - 1,
+ 0,
+ lastPartOriginal + after,
+ );
+ newCursorRow = newCursorRow + parts.length - 1;
+ newCursorCol = cpLen(lastPartOriginal);
+ } else {
+ newLines[newCursorRow] = before + parts[0] + after;
+ newCursorCol = cpLen(before) + cpLen(parts[0]);
+ }
+
+ return {
+ ...nextState,
+ lines: newLines,
+ cursorRow: newCursorRow,
+ cursorCol: newCursorCol,
+ preferredCol: null,
+ };
+ }
+
+ case 'backspace': {
+ const nextState = pushUndo(state);
+ const newLines = [...nextState.lines];
+ let newCursorRow = nextState.cursorRow;
+ let newCursorCol = nextState.cursorCol;
+
+ const currentLine = (r: number) => newLines[r] ?? '';
+
+ if (newCursorCol === 0 && newCursorRow === 0) return state;
+
+ if (newCursorCol > 0) {
+ const lineContent = currentLine(newCursorRow);
+ newLines[newCursorRow] =
+ cpSlice(lineContent, 0, newCursorCol - 1) +
+ cpSlice(lineContent, newCursorCol);
+ newCursorCol--;
+ } else if (newCursorRow > 0) {
+ const prevLineContent = currentLine(newCursorRow - 1);
+ const currentLineContentVal = currentLine(newCursorRow);
+ const newCol = cpLen(prevLineContent);
+ newLines[newCursorRow - 1] = prevLineContent + currentLineContentVal;
+ newLines.splice(newCursorRow, 1);
+ newCursorRow--;
+ newCursorCol = newCol;
+ }
+
+ return {
+ ...nextState,
+ lines: newLines,
+ cursorRow: newCursorRow,
+ cursorCol: newCursorCol,
+ preferredCol: null,
+ };
+ }
+
+ case 'set_viewport_width': {
+ if (action.payload === state.viewportWidth) {
+ return state;
+ }
+ return { ...state, viewportWidth: action.payload };
+ }
+
+ case 'move': {
+ const { dir } = action.payload;
+ const { lines, cursorRow, cursorCol, viewportWidth } = state;
+ const visualLayout = calculateVisualLayout(
+ lines,
+ [cursorRow, cursorCol],
+ viewportWidth,
+ );
+ const { visualLines, visualCursor, visualToLogicalMap } = visualLayout;
+
+ let newVisualRow = visualCursor[0];
+ let newVisualCol = visualCursor[1];
+ let newPreferredCol = state.preferredCol;
+
+ const currentVisLineLen = cpLen(visualLines[newVisualRow] ?? '');
+
+ switch (dir) {
+ case 'left':
+ newPreferredCol = null;
+ if (newVisualCol > 0) {
+ newVisualCol--;
+ } else if (newVisualRow > 0) {
+ newVisualRow--;
+ newVisualCol = cpLen(visualLines[newVisualRow] ?? '');
+ }
+ break;
+ case 'right':
+ newPreferredCol = null;
+ if (newVisualCol < currentVisLineLen) {
+ newVisualCol++;
+ } else if (newVisualRow < visualLines.length - 1) {
+ newVisualRow++;
+ newVisualCol = 0;
+ }
+ break;
+ case 'up':
+ if (newVisualRow > 0) {
+ if (newPreferredCol === null) newPreferredCol = newVisualCol;
+ newVisualRow--;
+ newVisualCol = clamp(
+ newPreferredCol,
+ 0,
+ cpLen(visualLines[newVisualRow] ?? ''),
+ );
+ }
+ break;
+ case 'down':
+ if (newVisualRow < visualLines.length - 1) {
+ if (newPreferredCol === null) newPreferredCol = newVisualCol;
+ newVisualRow++;
+ newVisualCol = clamp(
+ newPreferredCol,
+ 0,
+ cpLen(visualLines[newVisualRow] ?? ''),
+ );
+ }
+ break;
+ case 'home':
+ newPreferredCol = null;
+ newVisualCol = 0;
+ break;
+ case 'end':
+ newPreferredCol = null;
+ newVisualCol = currentVisLineLen;
+ break;
+ case 'wordLeft': {
+ const { cursorRow, cursorCol, lines } = state;
+ if (cursorCol === 0 && cursorRow === 0) return state;
+
+ let newCursorRow = cursorRow;
+ let newCursorCol = cursorCol;
+
+ if (cursorCol === 0) {
+ newCursorRow--;
+ newCursorCol = cpLen(lines[newCursorRow] ?? '');
+ } else {
+ const lineContent = lines[cursorRow];
+ const arr = toCodePoints(lineContent);
+ let start = cursorCol;
+ let onlySpaces = true;
+ for (let i = 0; i < start; i++) {
+ if (isWordChar(arr[i])) {
+ onlySpaces = false;
+ break;
+ }
+ }
+ if (onlySpaces && start > 0) {
+ start--;
+ } else {
+ while (start > 0 && !isWordChar(arr[start - 1])) start--;
+ while (start > 0 && isWordChar(arr[start - 1])) start--;
+ }
+ newCursorCol = start;
+ }
+ return {
+ ...state,
+ cursorRow: newCursorRow,
+ cursorCol: newCursorCol,
+ preferredCol: null,
+ };
+ }
+ case 'wordRight': {
+ const { cursorRow, cursorCol, lines } = state;
+ if (
+ cursorRow === lines.length - 1 &&
+ cursorCol === cpLen(lines[cursorRow] ?? '')
+ ) {
+ return state;
+ }
+
+ let newCursorRow = cursorRow;
+ let newCursorCol = cursorCol;
+ const lineContent = lines[cursorRow] ?? '';
+ const arr = toCodePoints(lineContent);
+
+ if (cursorCol >= arr.length) {
+ newCursorRow++;
+ newCursorCol = 0;
+ } else {
+ let end = cursorCol;
+ while (end < arr.length && !isWordChar(arr[end])) end++;
+ while (end < arr.length && isWordChar(arr[end])) end++;
+ newCursorCol = end;
+ }
+ return {
+ ...state,
+ cursorRow: newCursorRow,
+ cursorCol: newCursorCol,
+ preferredCol: null,
+ };
+ }
+ default:
+ break;
+ }
+
+ if (visualToLogicalMap[newVisualRow]) {
+ const [logRow, logStartCol] = visualToLogicalMap[newVisualRow];
+ return {
+ ...state,
+ cursorRow: logRow,
+ cursorCol: clamp(
+ logStartCol + newVisualCol,
+ 0,
+ cpLen(state.lines[logRow] ?? ''),
+ ),
+ preferredCol: newPreferredCol,
+ };
+ }
+ return state;
+ }
+
+ case 'delete': {
+ const { cursorRow, cursorCol, lines } = state;
+ const lineContent = currentLine(cursorRow);
+ if (cursorCol < currentLineLen(cursorRow)) {
+ const nextState = pushUndo(state);
+ const newLines = [...nextState.lines];
+ newLines[cursorRow] =
+ cpSlice(lineContent, 0, cursorCol) +
+ cpSlice(lineContent, cursorCol + 1);
+ return { ...nextState, lines: newLines, preferredCol: null };
+ } else if (cursorRow < lines.length - 1) {
+ const nextState = pushUndo(state);
+ const nextLineContent = currentLine(cursorRow + 1);
+ const newLines = [...nextState.lines];
+ newLines[cursorRow] = lineContent + nextLineContent;
+ newLines.splice(cursorRow + 1, 1);
+ return { ...nextState, lines: newLines, preferredCol: null };
+ }
+ return state;
+ }
+
+ case 'delete_word_left': {
+ const { cursorRow, cursorCol } = state;
+ if (cursorCol === 0 && cursorRow === 0) return state;
+ if (cursorCol === 0) {
+ // Act as a backspace
+ const nextState = pushUndo(state);
+ const prevLineContent = currentLine(cursorRow - 1);
+ const currentLineContentVal = currentLine(cursorRow);
+ const newCol = cpLen(prevLineContent);
+ const newLines = [...nextState.lines];
+ newLines[cursorRow - 1] = prevLineContent + currentLineContentVal;
+ newLines.splice(cursorRow, 1);
+ return {
+ ...nextState,
+ lines: newLines,
+ cursorRow: cursorRow - 1,
+ cursorCol: newCol,
+ preferredCol: null,
+ };
+ }
+ const nextState = pushUndo(state);
+ const lineContent = currentLine(cursorRow);
+ const arr = toCodePoints(lineContent);
+ let start = cursorCol;
+ let onlySpaces = true;
+ for (let i = 0; i < start; i++) {
+ if (isWordChar(arr[i])) {
+ onlySpaces = false;
+ break;
+ }
+ }
+ if (onlySpaces && start > 0) {
+ start--;
+ } else {
+ while (start > 0 && !isWordChar(arr[start - 1])) start--;
+ while (start > 0 && isWordChar(arr[start - 1])) start--;
+ }
+ const newLines = [...nextState.lines];
+ newLines[cursorRow] =
+ cpSlice(lineContent, 0, start) + cpSlice(lineContent, cursorCol);
+ return {
+ ...nextState,
+ lines: newLines,
+ cursorCol: start,
+ preferredCol: null,
+ };
+ }
+
+ case 'delete_word_right': {
+ const { cursorRow, cursorCol, lines } = state;
+ const lineContent = currentLine(cursorRow);
+ const arr = toCodePoints(lineContent);
+ if (cursorCol >= arr.length && cursorRow === lines.length - 1)
+ return state;
+ if (cursorCol >= arr.length) {
+ // Act as a delete
+ const nextState = pushUndo(state);
+ const nextLineContent = currentLine(cursorRow + 1);
+ const newLines = [...nextState.lines];
+ newLines[cursorRow] = lineContent + nextLineContent;
+ newLines.splice(cursorRow + 1, 1);
+ return { ...nextState, lines: newLines, preferredCol: null };
+ }
+ const nextState = pushUndo(state);
+ let end = cursorCol;
+ while (end < arr.length && !isWordChar(arr[end])) end++;
+ while (end < arr.length && isWordChar(arr[end])) end++;
+ const newLines = [...nextState.lines];
+ newLines[cursorRow] =
+ cpSlice(lineContent, 0, cursorCol) + cpSlice(lineContent, end);
+ return { ...nextState, lines: newLines, preferredCol: null };
+ }
+
+ case 'kill_line_right': {
+ const { cursorRow, cursorCol, lines } = state;
+ const lineContent = currentLine(cursorRow);
+ if (cursorCol < currentLineLen(cursorRow)) {
+ const nextState = pushUndo(state);
+ const newLines = [...nextState.lines];
+ newLines[cursorRow] = cpSlice(lineContent, 0, cursorCol);
+ return { ...nextState, lines: newLines };
+ } else if (cursorRow < lines.length - 1) {
+ // Act as a delete
+ const nextState = pushUndo(state);
+ const nextLineContent = currentLine(cursorRow + 1);
+ const newLines = [...nextState.lines];
+ newLines[cursorRow] = lineContent + nextLineContent;
+ newLines.splice(cursorRow + 1, 1);
+ return { ...nextState, lines: newLines, preferredCol: null };
+ }
+ return state;
+ }
+
+ case 'kill_line_left': {
+ const { cursorRow, cursorCol } = state;
+ if (cursorCol > 0) {
+ const nextState = pushUndo(state);
+ const lineContent = currentLine(cursorRow);
+ const newLines = [...nextState.lines];
+ newLines[cursorRow] = cpSlice(lineContent, cursorCol);
+ return {
+ ...nextState,
+ lines: newLines,
+ cursorCol: 0,
+ preferredCol: null,
+ };
+ }
+ return state;
+ }
+
+ case 'undo': {
+ const stateToRestore = state.undoStack[state.undoStack.length - 1];
+ if (!stateToRestore) return state;
+
+ const currentSnapshot = {
+ lines: [...state.lines],
+ cursorRow: state.cursorRow,
+ cursorCol: state.cursorCol,
+ };
+ return {
+ ...state,
+ ...stateToRestore,
+ undoStack: state.undoStack.slice(0, -1),
+ redoStack: [...state.redoStack, currentSnapshot],
+ };
+ }
+
+ case 'redo': {
+ const stateToRestore = state.redoStack[state.redoStack.length - 1];
+ if (!stateToRestore) return state;
+
+ const currentSnapshot = {
+ lines: [...state.lines],
+ cursorRow: state.cursorRow,
+ cursorCol: state.cursorCol,
+ };
+ return {
+ ...state,
+ ...stateToRestore,
+ redoStack: state.redoStack.slice(0, -1),
+ undoStack: [...state.undoStack, currentSnapshot],
+ };
+ }
+
+ case 'replace_range': {
+ const { startRow, startCol, endRow, endCol, text } = action.payload;
+ if (
+ startRow > endRow ||
+ (startRow === endRow && startCol > endCol) ||
+ startRow < 0 ||
+ startCol < 0 ||
+ endRow >= state.lines.length ||
+ (endRow < state.lines.length && endCol > currentLineLen(endRow))
+ ) {
+ return state; // Invalid range
+ }
+
+ const nextState = pushUndo(state);
+ const newLines = [...nextState.lines];
+
+ const sCol = clamp(startCol, 0, currentLineLen(startRow));
+ const eCol = clamp(endCol, 0, currentLineLen(endRow));
+
+ const prefix = cpSlice(currentLine(startRow), 0, sCol);
+ const suffix = cpSlice(currentLine(endRow), eCol);
+
+ const normalisedReplacement = text
+ .replace(/\r\n/g, '\n')
+ .replace(/\r/g, '\n');
+ const replacementParts = normalisedReplacement.split('\n');
+
+ // Replace the content
+ if (startRow === endRow) {
+ newLines[startRow] = prefix + normalisedReplacement + suffix;
+ } else {
+ const firstLine = prefix + replacementParts[0];
+ if (replacementParts.length === 1) {
+ // Single line of replacement text, but spanning multiple original lines
+ newLines.splice(startRow, endRow - startRow + 1, firstLine + suffix);
+ } else {
+ // Multi-line replacement text
+ const lastLine =
+ replacementParts[replacementParts.length - 1] + suffix;
+ const middleLines = replacementParts.slice(1, -1);
+ newLines.splice(
+ startRow,
+ endRow - startRow + 1,
+ firstLine,
+ ...middleLines,
+ lastLine,
+ );
+ }
+ }
+
+ const finalCursorRow = startRow + replacementParts.length - 1;
+ const finalCursorCol =
+ (replacementParts.length > 1 ? 0 : sCol) +
+ cpLen(replacementParts[replacementParts.length - 1]);
+
+ return {
+ ...nextState,
+ lines: newLines,
+ cursorRow: finalCursorRow,
+ cursorCol: finalCursorCol,
+ preferredCol: null,
+ };
+ }
+
+ case 'move_to_offset': {
+ const { offset } = action.payload;
+ const [newRow, newCol] = offsetToLogicalPos(
+ state.lines.join('\n'),
+ offset,
+ );
+ return {
+ ...state,
+ cursorRow: newRow,
+ cursorCol: newCol,
+ preferredCol: null,
+ };
+ }
+
+ case 'create_undo_snapshot': {
+ return pushUndo(state);
+ }
+
+ default: {
+ const exhaustiveCheck: never = action;
+ console.error(`Unknown action encountered: ${exhaustiveCheck}`);
+ return state;
+ }
+ }
+}
+
+// --- End of reducer logic ---
+
+export function useTextBuffer({
+ initialText = '',
+ initialCursorOffset = 0,
+ viewport,
+ stdin,
+ setRawMode,
+ onChange,
+ isValidPath,
+ shellModeActive = false,
+}: UseTextBufferProps): TextBuffer {
+ const initialState = useMemo((): TextBufferState => {
+ const lines = initialText.split('\n');
+ const [initialCursorRow, initialCursorCol] = calculateInitialCursorPosition(
+ lines.length === 0 ? [''] : lines,
+ initialCursorOffset,
+ );
+ return {
+ lines: lines.length === 0 ? [''] : lines,
+ cursorRow: initialCursorRow,
+ cursorCol: initialCursorCol,
+ preferredCol: null,
+ undoStack: [],
+ redoStack: [],
+ clipboard: null,
+ selectionAnchor: null,
+ viewportWidth: viewport.width,
+ };
+ }, [initialText, initialCursorOffset, viewport.width]);
+
+ const [state, dispatch] = useReducer(textBufferReducer, initialState);
+ const { lines, cursorRow, cursorCol, preferredCol, selectionAnchor } = state;
+
+ const text = useMemo(() => lines.join('\n'), [lines]);
+
+ const visualLayout = useMemo(
+ () =>
+ calculateVisualLayout(lines, [cursorRow, cursorCol], state.viewportWidth),
+ [lines, cursorRow, cursorCol, state.viewportWidth],
+ );
+
+ const { visualLines, visualCursor } = visualLayout;
+
+ const [visualScrollRow, setVisualScrollRow] = useState(0);
+
+ useEffect(() => {
+ if (onChange) {
+ onChange(text);
+ }
+ }, [text, onChange]);
+
+ useEffect(() => {
+ dispatch({ type: 'set_viewport_width', payload: viewport.width });
+ }, [viewport.width]);
+
+ // Update visual scroll (vertical)
+ useEffect(() => {
+ const { height } = viewport;
+ let newVisualScrollRow = visualScrollRow;
+
+ if (visualCursor[0] < visualScrollRow) {
+ newVisualScrollRow = visualCursor[0];
+ } else if (visualCursor[0] >= visualScrollRow + height) {
+ newVisualScrollRow = visualCursor[0] - height + 1;
+ }
+ if (newVisualScrollRow !== visualScrollRow) {
+ setVisualScrollRow(newVisualScrollRow);
+ }
+ }, [visualCursor, visualScrollRow, viewport]);
+
+ const insert = useCallback(
+ (ch: string): void => {
+ if (/[\n\r]/.test(ch)) {
+ dispatch({ type: 'insert', payload: ch });
+ return;
+ }
+
+ const minLengthToInferAsDragDrop = 3;
+ if (ch.length >= minLengthToInferAsDragDrop && !shellModeActive) {
+ let potentialPath = ch;
+ if (
+ potentialPath.length > 2 &&
+ potentialPath.startsWith("'") &&
+ potentialPath.endsWith("'")
+ ) {
+ potentialPath = ch.slice(1, -1);
+ }
+
+ potentialPath = potentialPath.trim();
+ if (isValidPath(unescapePath(potentialPath))) {
+ ch = `@${potentialPath}`;
+ }
+ }
+
+ let currentText = '';
+ for (const char of toCodePoints(ch)) {
+ if (char.codePointAt(0) === 127) {
+ if (currentText.length > 0) {
+ dispatch({ type: 'insert', payload: currentText });
+ currentText = '';
+ }
+ dispatch({ type: 'backspace' });
+ } else {
+ currentText += char;
+ }
+ }
+ if (currentText.length > 0) {
+ dispatch({ type: 'insert', payload: currentText });
+ }
+ },
+ [isValidPath, shellModeActive],
+ );
+
+ const newline = useCallback((): void => {
+ dispatch({ type: 'insert', payload: '\n' });
+ }, []);
+
+ const backspace = useCallback((): void => {
+ dispatch({ type: 'backspace' });
+ }, []);
+
+ const del = useCallback((): void => {
+ dispatch({ type: 'delete' });
+ }, []);
+
+ const move = useCallback((dir: Direction): void => {
+ dispatch({ type: 'move', payload: { dir } });
+ }, []);
+
+ const undo = useCallback((): void => {
+ dispatch({ type: 'undo' });
+ }, []);
+
+ const redo = useCallback((): void => {
+ dispatch({ type: 'redo' });
+ }, []);
+
+ const setText = useCallback((newText: string): void => {
+ dispatch({ type: 'set_text', payload: newText });
+ }, []);
+
+ const deleteWordLeft = useCallback((): void => {
+ dispatch({ type: 'delete_word_left' });
+ }, []);
+
+ const deleteWordRight = useCallback((): void => {
+ dispatch({ type: 'delete_word_right' });
+ }, []);
+
+ const killLineRight = useCallback((): void => {
+ dispatch({ type: 'kill_line_right' });
+ }, []);
+
+ const killLineLeft = useCallback((): void => {
+ dispatch({ type: 'kill_line_left' });
+ }, []);
+
+ const openInExternalEditor = useCallback(
+ async (opts: { editor?: string } = {}): Promise => {
+ const editor =
+ opts.editor ??
+ process.env['VISUAL'] ??
+ process.env['EDITOR'] ??
+ (process.platform === 'win32' ? 'notepad' : 'vi');
+ const tmpDir = fs.mkdtempSync(pathMod.join(os.tmpdir(), 'gemini-edit-'));
+ const filePath = pathMod.join(tmpDir, 'buffer.txt');
+ fs.writeFileSync(filePath, text, 'utf8');
+
+ dispatch({ type: 'create_undo_snapshot' });
+
+ const wasRaw = stdin?.isRaw ?? false;
+ try {
+ setRawMode?.(false);
+ const { status, error } = spawnSync(editor, [filePath], {
+ stdio: 'inherit',
+ });
+ if (error) throw error;
+ if (typeof status === 'number' && status !== 0)
+ throw new Error(`External editor exited with status ${status}`);
+
+ let newText = fs.readFileSync(filePath, 'utf8');
+ newText = newText.replace(/\r\n?/g, '\n');
+ dispatch({ type: 'set_text', payload: newText, pushToUndo: false });
+ } catch (err) {
+ console.error('[useTextBuffer] external editor error', err);
+ } finally {
+ if (wasRaw) setRawMode?.(true);
+ try {
+ fs.unlinkSync(filePath);
+ } catch {
+ /* ignore */
+ }
+ try {
+ fs.rmdirSync(tmpDir);
+ } catch {
+ /* ignore */
+ }
+ }
+ },
+ [text, stdin, setRawMode],
+ );
+
+ const handleInput = useCallback(
+ (key: {
+ name: string;
+ ctrl: boolean;
+ meta: boolean;
+ shift: boolean;
+ paste: boolean;
+ sequence: string;
+ }): void => {
+ const { sequence: input } = key;
+
+ if (
+ key.name === 'return' ||
+ input === '\r' ||
+ input === '\n' ||
+ input === '\\\r' // VSCode terminal represents shift + enter this way
+ )
+ newline();
+ else if (key.name === 'left' && !key.meta && !key.ctrl) move('left');
+ else if (key.ctrl && key.name === 'b') move('left');
+ else if (key.name === 'right' && !key.meta && !key.ctrl) move('right');
+ else if (key.ctrl && key.name === 'f') move('right');
+ else if (key.name === 'up') move('up');
+ else if (key.name === 'down') move('down');
+ else if ((key.ctrl || key.meta) && key.name === 'left') move('wordLeft');
+ else if (key.meta && key.name === 'b') move('wordLeft');
+ else if ((key.ctrl || key.meta) && key.name === 'right')
+ move('wordRight');
+ else if (key.meta && key.name === 'f') move('wordRight');
+ else if (key.name === 'home') move('home');
+ else if (key.ctrl && key.name === 'a') move('home');
+ else if (key.name === 'end') move('end');
+ else if (key.ctrl && key.name === 'e') move('end');
+ else if (key.ctrl && key.name === 'w') deleteWordLeft();
+ else if (
+ (key.meta || key.ctrl) &&
+ (key.name === 'backspace' || input === '\x7f')
+ )
+ deleteWordLeft();
+ else if ((key.meta || key.ctrl) && key.name === 'delete')
+ deleteWordRight();
+ else if (
+ key.name === 'backspace' ||
+ input === '\x7f' ||
+ (key.ctrl && key.name === 'h')
+ )
+ backspace();
+ else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del();
+ else if (input && !key.ctrl && !key.meta) {
+ insert(input);
+ }
+ },
+ [newline, move, deleteWordLeft, deleteWordRight, backspace, del, insert],
+ );
+
+ const renderedVisualLines = useMemo(
+ () => visualLines.slice(visualScrollRow, visualScrollRow + viewport.height),
+ [visualLines, visualScrollRow, viewport.height],
+ );
+
+ const replaceRange = useCallback(
+ (
+ startRow: number,
+ startCol: number,
+ endRow: number,
+ endCol: number,
+ text: string,
+ ): void => {
+ dispatch({
+ type: 'replace_range',
+ payload: { startRow, startCol, endRow, endCol, text },
+ });
+ },
+ [],
+ );
+
+ const replaceRangeByOffset = useCallback(
+ (startOffset: number, endOffset: number, replacementText: string): void => {
+ const [startRow, startCol] = offsetToLogicalPos(text, startOffset);
+ const [endRow, endCol] = offsetToLogicalPos(text, endOffset);
+ replaceRange(startRow, startCol, endRow, endCol, replacementText);
+ },
+ [text, replaceRange],
+ );
+
+ const moveToOffset = useCallback((offset: number): void => {
+ dispatch({ type: 'move_to_offset', payload: { offset } });
+ }, []);
+
+ const returnValue: TextBuffer = {
+ lines,
+ text,
+ cursor: [cursorRow, cursorCol],
+ preferredCol,
+ selectionAnchor,
+
+ allVisualLines: visualLines,
+ viewportVisualLines: renderedVisualLines,
+ visualCursor,
+ visualScrollRow,
+
+ setText,
+ insert,
+ newline,
+ backspace,
+ del,
+ move,
+ undo,
+ redo,
+ replaceRange,
+ replaceRangeByOffset,
+ moveToOffset,
+ deleteWordLeft,
+ deleteWordRight,
+ killLineRight,
+ killLineLeft,
+ handleInput,
+ openInExternalEditor,
+ };
+ return returnValue;
+}
+
+export interface TextBuffer {
+ // State
+ lines: string[]; // Logical lines
+ text: string;
+ cursor: [number, number]; // Logical cursor [row, col]
+ /**
+ * When the user moves the caret vertically we try to keep their original
+ * horizontal column even when passing through shorter lines. We remember
+ * that *preferred* column in this field while the user is still travelling
+ * vertically. Any explicit horizontal movement resets the preference.
+ */
+ preferredCol: number | null; // Preferred visual column
+ selectionAnchor: [number, number] | null; // Logical selection anchor
+
+ // Visual state (handles wrapping)
+ allVisualLines: string[]; // All visual lines for the current text and viewport width.
+ viewportVisualLines: string[]; // The subset of visual lines to be rendered based on visualScrollRow and viewport.height
+ visualCursor: [number, number]; // Visual cursor [row, col] relative to the start of all visualLines
+ visualScrollRow: number; // Scroll position for visual lines (index of the first visible visual line)
+
+ // Actions
+
+ /**
+ * Replaces the entire buffer content with the provided text.
+ * The operation is undoable.
+ */
+ setText: (text: string) => void;
+ /**
+ * Insert a single character or string without newlines.
+ */
+ insert: (ch: string) => void;
+ newline: () => void;
+ backspace: () => void;
+ del: () => void;
+ move: (dir: Direction) => void;
+ undo: () => void;
+ redo: () => void;
+ /**
+ * Replaces the text within the specified range with new text.
+ * Handles both single-line and multi-line ranges.
+ *
+ * @param startRow The starting row index (inclusive).
+ * @param startCol The starting column index (inclusive, code-point based).
+ * @param endRow The ending row index (inclusive).
+ * @param endCol The ending column index (exclusive, code-point based).
+ * @param text The new text to insert.
+ * @returns True if the buffer was modified, false otherwise.
+ */
+ replaceRange: (
+ startRow: number,
+ startCol: number,
+ endRow: number,
+ endCol: number,
+ text: string,
+ ) => void;
+ /**
+ * Delete the word to the *left* of the caret, mirroring common
+ * Ctrl/Alt+Backspace behaviour in editors & terminals. Both the adjacent
+ * whitespace *and* the word characters immediately preceding the caret are
+ * removed. If the caret is already at columnโ0 this becomes a no-op.
+ */
+ deleteWordLeft: () => void;
+ /**
+ * Delete the word to the *right* of the caret, akin to many editors'
+ * Ctrl/Alt+Delete shortcut. Removes any whitespace/punctuation that
+ * follows the caret and the next contiguous run of word characters.
+ */
+ deleteWordRight: () => void;
+ /**
+ * Deletes text from the cursor to the end of the current line.
+ */
+ killLineRight: () => void;
+ /**
+ * Deletes text from the start of the current line to the cursor.
+ */
+ killLineLeft: () => void;
+ /**
+ * High level "handleInput" โ receives what Ink gives us.
+ */
+ handleInput: (key: {
+ name: string;
+ ctrl: boolean;
+ meta: boolean;
+ shift: boolean;
+ paste: boolean;
+ sequence: string;
+ }) => void;
+ /**
+ * Opens the current buffer contents in the user's preferred terminal text
+ * editor ($VISUAL or $EDITOR, falling back to "vi"). The method blocks
+ * until the editor exits, then reloads the file and replaces the inโmemory
+ * buffer with whatever the user saved.
+ *
+ * The operation is treated as a single undoable edit โ we snapshot the
+ * previous state *once* before launching the editor so one `undo()` will
+ * revert the entire change set.
+ *
+ * Note: We purposefully rely on the *synchronous* spawn API so that the
+ * calling process genuinely waits for the editor to close before
+ * continuing. This mirrors Git's behaviour and simplifies downstream
+ * controlโflow (callers can simply `await` the Promise).
+ */
+ openInExternalEditor: (opts?: { editor?: string }) => Promise;
+
+ replaceRangeByOffset: (
+ startOffset: number,
+ endOffset: number,
+ replacementText: string,
+ ) => void;
+ moveToOffset(offset: number): void;
+}
diff --git a/packages/cli/src/ui/constants.ts b/packages/cli/src/ui/constants.ts
new file mode 100644
index 00000000..6a0a9375
--- /dev/null
+++ b/packages/cli/src/ui/constants.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const EstimatedArtWidth = 59;
+const BoxBorderWidth = 1;
+export const BOX_PADDING_X = 1;
+
+// Calculate width based on art, padding, and border
+export const UI_WIDTH =
+ EstimatedArtWidth + BOX_PADDING_X * 2 + BoxBorderWidth * 2; // ~63
+
+export const STREAM_DEBOUNCE_MS = 100;
diff --git a/packages/cli/src/ui/contexts/OverflowContext.tsx b/packages/cli/src/ui/contexts/OverflowContext.tsx
new file mode 100644
index 00000000..f21a4e0f
--- /dev/null
+++ b/packages/cli/src/ui/contexts/OverflowContext.tsx
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ useMemo,
+} from 'react';
+
+interface OverflowState {
+ overflowingIds: ReadonlySet;
+}
+
+interface OverflowActions {
+ addOverflowingId: (id: string) => void;
+ removeOverflowingId: (id: string) => void;
+}
+
+const OverflowStateContext = createContext(
+ undefined,
+);
+
+const OverflowActionsContext = createContext(
+ undefined,
+);
+
+export const useOverflowState = (): OverflowState | undefined =>
+ useContext(OverflowStateContext);
+
+export const useOverflowActions = (): OverflowActions | undefined =>
+ useContext(OverflowActionsContext);
+
+export const OverflowProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [overflowingIds, setOverflowingIds] = useState(new Set());
+
+ const addOverflowingId = useCallback((id: string) => {
+ setOverflowingIds((prevIds) => {
+ if (prevIds.has(id)) {
+ return prevIds;
+ }
+ const newIds = new Set(prevIds);
+ newIds.add(id);
+ return newIds;
+ });
+ }, []);
+
+ const removeOverflowingId = useCallback((id: string) => {
+ setOverflowingIds((prevIds) => {
+ if (!prevIds.has(id)) {
+ return prevIds;
+ }
+ const newIds = new Set(prevIds);
+ newIds.delete(id);
+ return newIds;
+ });
+ }, []);
+
+ const stateValue = useMemo(
+ () => ({
+ overflowingIds,
+ }),
+ [overflowingIds],
+ );
+
+ const actionsValue = useMemo(
+ () => ({
+ addOverflowingId,
+ removeOverflowingId,
+ }),
+ [addOverflowingId, removeOverflowingId],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/packages/cli/src/ui/contexts/SessionContext.test.tsx b/packages/cli/src/ui/contexts/SessionContext.test.tsx
new file mode 100644
index 00000000..71bf9ca1
--- /dev/null
+++ b/packages/cli/src/ui/contexts/SessionContext.test.tsx
@@ -0,0 +1,132 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { type MutableRefObject } from 'react';
+import { render } from 'ink-testing-library';
+import { renderHook } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
+import {
+ SessionStatsProvider,
+ useSessionStats,
+ SessionMetrics,
+} from './SessionContext.js';
+import { describe, it, expect, vi } from 'vitest';
+import { uiTelemetryService } from '@qwen/qwen-code-core';
+
+/**
+ * A test harness component that uses the hook and exposes the context value
+ * via a mutable ref. This allows us to interact with the context's functions
+ * and assert against its state directly in our tests.
+ */
+const TestHarness = ({
+ contextRef,
+}: {
+ contextRef: MutableRefObject | undefined>;
+}) => {
+ contextRef.current = useSessionStats();
+ return null;
+};
+
+describe('SessionStatsContext', () => {
+ it('should provide the correct initial state', () => {
+ const contextRef: MutableRefObject<
+ ReturnType | undefined
+ > = { current: undefined };
+
+ render(
+
+
+ ,
+ );
+
+ const stats = contextRef.current?.stats;
+
+ expect(stats?.sessionStartTime).toBeInstanceOf(Date);
+ expect(stats?.metrics).toBeDefined();
+ expect(stats?.metrics.models).toEqual({});
+ });
+
+ it('should update metrics when the uiTelemetryService emits an update', () => {
+ const contextRef: MutableRefObject<
+ ReturnType | undefined
+ > = { current: undefined };
+
+ render(
+
+
+ ,
+ );
+
+ const newMetrics: SessionMetrics = {
+ models: {
+ 'gemini-pro': {
+ api: {
+ totalRequests: 1,
+ totalErrors: 0,
+ totalLatencyMs: 123,
+ },
+ tokens: {
+ prompt: 100,
+ candidates: 200,
+ total: 300,
+ cached: 50,
+ thoughts: 20,
+ tool: 10,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 1,
+ totalSuccess: 1,
+ totalFail: 0,
+ totalDurationMs: 456,
+ totalDecisions: {
+ accept: 1,
+ reject: 0,
+ modify: 0,
+ },
+ byName: {
+ 'test-tool': {
+ count: 1,
+ success: 1,
+ fail: 0,
+ durationMs: 456,
+ decisions: {
+ accept: 1,
+ reject: 0,
+ modify: 0,
+ },
+ },
+ },
+ },
+ };
+
+ act(() => {
+ uiTelemetryService.emit('update', {
+ metrics: newMetrics,
+ lastPromptTokenCount: 100,
+ });
+ });
+
+ const stats = contextRef.current?.stats;
+ expect(stats?.metrics).toEqual(newMetrics);
+ expect(stats?.lastPromptTokenCount).toBe(100);
+ });
+
+ it('should throw an error when useSessionStats is used outside of a provider', () => {
+ // Suppress console.error for this test since we expect an error
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ try {
+ // Expect renderHook itself to throw when the hook is used outside a provider
+ expect(() => {
+ renderHook(() => useSessionStats());
+ }).toThrow('useSessionStats must be used within a SessionStatsProvider');
+ } finally {
+ consoleSpy.mockRestore();
+ }
+ });
+});
diff --git a/packages/cli/src/ui/contexts/SessionContext.tsx b/packages/cli/src/ui/contexts/SessionContext.tsx
new file mode 100644
index 00000000..4ca7bc4a
--- /dev/null
+++ b/packages/cli/src/ui/contexts/SessionContext.tsx
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useState,
+ useMemo,
+ useEffect,
+} from 'react';
+
+import {
+ uiTelemetryService,
+ SessionMetrics,
+ ModelMetrics,
+} from '@qwen/qwen-code-core';
+
+// --- Interface Definitions ---
+
+export type { SessionMetrics, ModelMetrics };
+
+export interface SessionStatsState {
+ sessionStartTime: Date;
+ metrics: SessionMetrics;
+ lastPromptTokenCount: number;
+ promptCount: number;
+}
+
+export interface ComputedSessionStats {
+ totalApiTime: number;
+ totalToolTime: number;
+ agentActiveTime: number;
+ apiTimePercent: number;
+ toolTimePercent: number;
+ cacheEfficiency: number;
+ totalDecisions: number;
+ successRate: number;
+ agreementRate: number;
+ totalCachedTokens: number;
+ totalPromptTokens: number;
+}
+
+// Defines the final "value" of our context, including the state
+// and the functions to update it.
+interface SessionStatsContextValue {
+ stats: SessionStatsState;
+ startNewPrompt: () => void;
+ getPromptCount: () => number;
+}
+
+// --- Context Definition ---
+
+const SessionStatsContext = createContext(
+ undefined,
+);
+
+// --- Provider Component ---
+
+export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [stats, setStats] = useState({
+ sessionStartTime: new Date(),
+ metrics: uiTelemetryService.getMetrics(),
+ lastPromptTokenCount: 0,
+ promptCount: 0,
+ });
+
+ useEffect(() => {
+ const handleUpdate = ({
+ metrics,
+ lastPromptTokenCount,
+ }: {
+ metrics: SessionMetrics;
+ lastPromptTokenCount: number;
+ }) => {
+ setStats((prevState) => ({
+ ...prevState,
+ metrics,
+ lastPromptTokenCount,
+ }));
+ };
+
+ uiTelemetryService.on('update', handleUpdate);
+ // Set initial state
+ handleUpdate({
+ metrics: uiTelemetryService.getMetrics(),
+ lastPromptTokenCount: uiTelemetryService.getLastPromptTokenCount(),
+ });
+
+ return () => {
+ uiTelemetryService.off('update', handleUpdate);
+ };
+ }, []);
+
+ const startNewPrompt = useCallback(() => {
+ setStats((prevState) => ({
+ ...prevState,
+ promptCount: prevState.promptCount + 1,
+ }));
+ }, []);
+
+ const getPromptCount = useCallback(
+ () => stats.promptCount,
+ [stats.promptCount],
+ );
+
+ const value = useMemo(
+ () => ({
+ stats,
+ startNewPrompt,
+ getPromptCount,
+ }),
+ [stats, startNewPrompt, getPromptCount],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+// --- Consumer Hook ---
+
+export const useSessionStats = () => {
+ const context = useContext(SessionStatsContext);
+ if (context === undefined) {
+ throw new Error(
+ 'useSessionStats must be used within a SessionStatsProvider',
+ );
+ }
+ return context;
+};
diff --git a/packages/cli/src/ui/contexts/StreamingContext.tsx b/packages/cli/src/ui/contexts/StreamingContext.tsx
new file mode 100644
index 00000000..8944d682
--- /dev/null
+++ b/packages/cli/src/ui/contexts/StreamingContext.tsx
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { createContext } from 'react';
+import { StreamingState } from '../types.js';
+
+export const StreamingContext = createContext(
+ undefined,
+);
+
+export const useStreamingContext = (): StreamingState => {
+ const context = React.useContext(StreamingContext);
+ if (context === undefined) {
+ throw new Error(
+ 'useStreamingContext must be used within a StreamingContextProvider',
+ );
+ }
+ return context;
+};
diff --git a/packages/cli/src/ui/editors/editorSettingsManager.ts b/packages/cli/src/ui/editors/editorSettingsManager.ts
new file mode 100644
index 00000000..41550e8c
--- /dev/null
+++ b/packages/cli/src/ui/editors/editorSettingsManager.ts
@@ -0,0 +1,71 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ allowEditorTypeInSandbox,
+ checkHasEditorType,
+ type EditorType,
+} from '@qwen/qwen-code-core';
+
+export interface EditorDisplay {
+ name: string;
+ type: EditorType | 'not_set';
+ disabled: boolean;
+}
+
+export const EDITOR_DISPLAY_NAMES: Record = {
+ zed: 'Zed',
+ vscode: 'VS Code',
+ vscodium: 'VSCodium',
+ windsurf: 'Windsurf',
+ cursor: 'Cursor',
+ vim: 'Vim',
+ neovim: 'Neovim',
+};
+
+class EditorSettingsManager {
+ private readonly availableEditors: EditorDisplay[];
+
+ constructor() {
+ const editorTypes: EditorType[] = [
+ 'zed',
+ 'vscode',
+ 'vscodium',
+ 'windsurf',
+ 'cursor',
+ 'vim',
+ 'neovim',
+ ];
+ this.availableEditors = [
+ {
+ name: 'None',
+ type: 'not_set',
+ disabled: false,
+ },
+ ...editorTypes.map((type) => {
+ const hasEditor = checkHasEditorType(type);
+ const isAllowedInSandbox = allowEditorTypeInSandbox(type);
+
+ let labelSuffix = !isAllowedInSandbox
+ ? ' (Not available in sandbox)'
+ : '';
+ labelSuffix = !hasEditor ? ' (Not installed)' : labelSuffix;
+
+ return {
+ name: EDITOR_DISPLAY_NAMES[type] + labelSuffix,
+ type,
+ disabled: !hasEditor || !isAllowedInSandbox,
+ };
+ }),
+ ];
+ }
+
+ getAvailableEditorDisplays(): EditorDisplay[] {
+ return this.availableEditors;
+ }
+}
+
+export const editorSettingsManager = new EditorSettingsManager();
diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts
new file mode 100644
index 00000000..dc89d4a0
--- /dev/null
+++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts
@@ -0,0 +1,762 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
+import type { Mocked } from 'vitest';
+import { handleAtCommand } from './atCommandProcessor.js';
+import { Config, FileDiscoveryService } from '@qwen/qwen-code-core';
+import { ToolCallStatus } from '../types.js';
+import { UseHistoryManagerReturn } from './useHistoryManager.js';
+import * as fsPromises from 'fs/promises';
+import type { Stats } from 'fs';
+
+const mockGetToolRegistry = vi.fn();
+const mockGetTargetDir = vi.fn();
+const mockConfig = {
+ getToolRegistry: mockGetToolRegistry,
+ getTargetDir: mockGetTargetDir,
+ isSandboxed: vi.fn(() => false),
+ getFileService: vi.fn(),
+ getFileFilteringRespectGitIgnore: vi.fn(() => true),
+ getEnableRecursiveFileSearch: vi.fn(() => true),
+} as unknown as Config;
+
+const mockReadManyFilesExecute = vi.fn();
+const mockReadManyFilesTool = {
+ name: 'read_many_files',
+ displayName: 'Read Many Files',
+ description: 'Reads multiple files.',
+ execute: mockReadManyFilesExecute,
+ getDescription: vi.fn((params) => `Read files: ${params.paths.join(', ')}`),
+};
+
+const mockGlobExecute = vi.fn();
+const mockGlobTool = {
+ name: 'glob',
+ displayName: 'Glob Tool',
+ execute: mockGlobExecute,
+ getDescription: vi.fn(() => 'Glob tool description'),
+};
+
+const mockAddItem: Mock = vi.fn();
+const mockOnDebugMessage: Mock<(message: string) => void> = vi.fn();
+
+vi.mock('fs/promises', async () => {
+ const actual = await vi.importActual('fs/promises');
+ return {
+ ...actual,
+ stat: vi.fn(),
+ };
+});
+
+vi.mock('@qwen/qwen-code-core', async () => {
+ const actual = await vi.importActual('@qwen/qwen-code-core');
+ return {
+ ...actual,
+ FileDiscoveryService: vi.fn(),
+ };
+});
+
+describe('handleAtCommand', () => {
+ let abortController: AbortController;
+ let mockFileDiscoveryService: Mocked;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ abortController = new AbortController();
+ mockGetTargetDir.mockReturnValue('/test/dir');
+ mockGetToolRegistry.mockReturnValue({
+ getTool: vi.fn((toolName: string) => {
+ if (toolName === 'read_many_files') return mockReadManyFilesTool;
+ if (toolName === 'glob') return mockGlobTool;
+ return undefined;
+ }),
+ });
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ isDirectory: () => false,
+ } as Stats);
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: '',
+ returnDisplay: '',
+ });
+ mockGlobExecute.mockResolvedValue({
+ llmContent: 'No files found',
+ returnDisplay: '',
+ });
+
+ // Mock FileDiscoveryService
+ mockFileDiscoveryService = {
+ initialize: vi.fn(),
+ shouldIgnoreFile: vi.fn(() => false),
+ filterFiles: vi.fn((files) => files),
+ getIgnoreInfo: vi.fn(() => ({ gitIgnored: [] })),
+ isGitRepository: vi.fn(() => true),
+ };
+ vi.mocked(FileDiscoveryService).mockImplementation(
+ () => mockFileDiscoveryService,
+ );
+
+ // Mock getFileService to return the mocked FileDiscoveryService
+ mockConfig.getFileService = vi
+ .fn()
+ .mockReturnValue(mockFileDiscoveryService);
+ });
+
+ afterEach(() => {
+ abortController.abort();
+ });
+
+ it('should pass through query if no @ command is present', async () => {
+ const query = 'regular user query';
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 123,
+ signal: abortController.signal,
+ });
+ expect(mockAddItem).toHaveBeenCalledWith(
+ { type: 'user', text: query },
+ 123,
+ );
+ expect(result.processedQuery).toEqual([{ text: query }]);
+ expect(result.shouldProceed).toBe(true);
+ expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
+ });
+
+ it('should pass through original query if only a lone @ symbol is present', async () => {
+ const queryWithSpaces = ' @ ';
+ const result = await handleAtCommand({
+ query: queryWithSpaces,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 124,
+ signal: abortController.signal,
+ });
+ expect(mockAddItem).toHaveBeenCalledWith(
+ { type: 'user', text: queryWithSpaces },
+ 124,
+ );
+ expect(result.processedQuery).toEqual([{ text: queryWithSpaces }]);
+ expect(result.shouldProceed).toBe(true);
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ 'Lone @ detected, will be treated as text in the modified query.',
+ );
+ });
+
+ it('should process a valid text file path', async () => {
+ const filePath = 'path/to/file.txt';
+ const query = `@${filePath}`;
+ const fileContent = 'This is the file content.';
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${filePath} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read 1 file.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 125,
+ signal: abortController.signal,
+ });
+ expect(mockAddItem).toHaveBeenCalledWith(
+ { type: 'user', text: query },
+ 125,
+ );
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ { paths: [filePath], respect_git_ignore: true },
+ abortController.signal,
+ );
+ expect(mockAddItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'tool_group',
+ tools: [expect.objectContaining({ status: ToolCallStatus.Success })],
+ }),
+ 125,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `@${filePath}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${filePath}:\n` },
+ { text: fileContent },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should process a valid directory path and convert to glob', async () => {
+ const dirPath = 'path/to/dir';
+ const query = `@${dirPath}`;
+ const resolvedGlob = `${dirPath}/**`;
+ const fileContent = 'Directory content.';
+ vi.mocked(fsPromises.stat).mockResolvedValue({
+ isDirectory: () => true,
+ } as Stats);
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${resolvedGlob} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read directory contents.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 126,
+ signal: abortController.signal,
+ });
+ expect(mockAddItem).toHaveBeenCalledWith(
+ { type: 'user', text: query },
+ 126,
+ );
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ { paths: [resolvedGlob], respect_git_ignore: true },
+ abortController.signal,
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Path ${dirPath} resolved to directory, using glob: ${resolvedGlob}`,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `@${resolvedGlob}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${resolvedGlob}:\n` },
+ { text: fileContent },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should process a valid image file path (as text content for now)', async () => {
+ const imagePath = 'path/to/image.png';
+ const query = `@${imagePath}`;
+ // For @-commands, read_many_files is expected to return text or structured text.
+ // If it were to return actual image Part, the test and handling would be different.
+ // Current implementation of read_many_files for images returns base64 in text.
+ const imageFileTextContent = '[base64 image data for path/to/image.png]';
+ const imagePart = {
+ mimeType: 'image/png',
+ inlineData: imageFileTextContent,
+ };
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [imagePart],
+ returnDisplay: 'Read 1 image.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 127,
+ signal: abortController.signal,
+ });
+ expect(result.processedQuery).toEqual([
+ { text: `@${imagePath}` },
+ { text: '\n--- Content from referenced files ---' },
+ imagePart,
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should handle query with text before and after @command', async () => {
+ const textBefore = 'Explain this: ';
+ const filePath = 'doc.md';
+ const textAfter = ' in detail.';
+ const query = `${textBefore}@${filePath}${textAfter}`;
+ const fileContent = 'Markdown content.';
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${filePath} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read 1 doc.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 128,
+ signal: abortController.signal,
+ });
+ expect(mockAddItem).toHaveBeenCalledWith(
+ { type: 'user', text: query },
+ 128,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `${textBefore}@${filePath}${textAfter}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${filePath}:\n` },
+ { text: fileContent },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should correctly unescape paths with escaped spaces', async () => {
+ const rawPath = 'path/to/my\\ file.txt';
+ const unescapedPath = 'path/to/my file.txt';
+ const query = `@${rawPath}`;
+ const fileContent = 'Content of file with space.';
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${unescapedPath} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read 1 file.',
+ });
+
+ await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 129,
+ signal: abortController.signal,
+ });
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ { paths: [unescapedPath], respect_git_ignore: true },
+ abortController.signal,
+ );
+ });
+
+ it('should handle multiple @file references', async () => {
+ const file1 = 'file1.txt';
+ const content1 = 'Content file1';
+ const file2 = 'file2.md';
+ const content2 = 'Content file2';
+ const query = `@${file1} @${file2}`;
+
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [
+ `--- ${file1} ---\n\n${content1}\n\n`,
+ `--- ${file2} ---\n\n${content2}\n\n`,
+ ],
+ returnDisplay: 'Read 2 files.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 130,
+ signal: abortController.signal,
+ });
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ { paths: [file1, file2], respect_git_ignore: true },
+ abortController.signal,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `@${file1} @${file2}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${file1}:\n` },
+ { text: content1 },
+ { text: `\nContent from @${file2}:\n` },
+ { text: content2 },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should handle multiple @file references with interleaved text', async () => {
+ const text1 = 'Check ';
+ const file1 = 'f1.txt';
+ const content1 = 'C1';
+ const text2 = ' and ';
+ const file2 = 'f2.md';
+ const content2 = 'C2';
+ const text3 = ' please.';
+ const query = `${text1}@${file1}${text2}@${file2}${text3}`;
+
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [
+ `--- ${file1} ---\n\n${content1}\n\n`,
+ `--- ${file2} ---\n\n${content2}\n\n`,
+ ],
+ returnDisplay: 'Read 2 files.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 131,
+ signal: abortController.signal,
+ });
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ { paths: [file1, file2], respect_git_ignore: true },
+ abortController.signal,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `${text1}@${file1}${text2}@${file2}${text3}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${file1}:\n` },
+ { text: content1 },
+ { text: `\nContent from @${file2}:\n` },
+ { text: content2 },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should handle a mix of valid, invalid, and lone @ references', async () => {
+ const file1 = 'valid1.txt';
+ const content1 = 'Valid content 1';
+ const invalidFile = 'nonexistent.txt';
+ const query = `Look at @${file1} then @${invalidFile} and also just @ symbol, then @valid2.glob`;
+ const file2Glob = 'valid2.glob';
+ const resolvedFile2 = 'resolved/valid2.actual';
+ const content2 = 'Globbed content';
+
+ // Mock fs.stat for file1 (valid)
+ vi.mocked(fsPromises.stat).mockImplementation(async (p) => {
+ if (p.toString().endsWith(file1))
+ return { isDirectory: () => false } as Stats;
+ if (p.toString().endsWith(invalidFile))
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
+ // For valid2.glob, stat will fail, triggering glob
+ if (p.toString().endsWith(file2Glob))
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
+ return { isDirectory: () => false } as Stats; // Default
+ });
+
+ // Mock glob to find resolvedFile2 for valid2.glob
+ mockGlobExecute.mockImplementation(async (params) => {
+ if (params.pattern.includes('valid2.glob')) {
+ return {
+ llmContent: `Found files:\n${mockGetTargetDir()}/${resolvedFile2}`,
+ returnDisplay: 'Found 1 file',
+ };
+ }
+ return { llmContent: 'No files found', returnDisplay: '' };
+ });
+
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [
+ `--- ${file1} ---\n\n${content1}\n\n`,
+ `--- ${resolvedFile2} ---\n\n${content2}\n\n`,
+ ],
+ returnDisplay: 'Read 2 files.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 132,
+ signal: abortController.signal,
+ });
+
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ { paths: [file1, resolvedFile2], respect_git_ignore: true },
+ abortController.signal,
+ );
+ expect(result.processedQuery).toEqual([
+ // Original query has @nonexistent.txt and @, but resolved has @resolved/valid2.actual
+ {
+ text: `Look at @${file1} then @${invalidFile} and also just @ symbol, then @${resolvedFile2}`,
+ },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${file1}:\n` },
+ { text: content1 },
+ { text: `\nContent from @${resolvedFile2}:\n` },
+ { text: content2 },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Path ${invalidFile} not found directly, attempting glob search.`,
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Glob search for '**/*${invalidFile}*' found no files or an error. Path ${invalidFile} will be skipped.`,
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ 'Lone @ detected, will be treated as text in the modified query.',
+ );
+ });
+
+ it('should return original query if all @paths are invalid or lone @', async () => {
+ const query = 'Check @nonexistent.txt and @ also';
+ vi.mocked(fsPromises.stat).mockRejectedValue(
+ Object.assign(new Error('ENOENT'), { code: 'ENOENT' }),
+ );
+ mockGlobExecute.mockResolvedValue({
+ llmContent: 'No files found',
+ returnDisplay: '',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 133,
+ signal: abortController.signal,
+ });
+ expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
+ // The modified query string will be "Check @nonexistent.txt and @ also" because no paths were resolved for reading.
+ expect(result.processedQuery).toEqual([
+ { text: 'Check @nonexistent.txt and @ also' },
+ ]);
+
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should process a file path case-insensitively', async () => {
+ // const actualFilePath = 'path/to/MyFile.txt'; // Unused, path in llmContent should match queryPath
+ const queryPath = 'path/to/myfile.txt'; // Different case
+ const query = `@${queryPath}`;
+ const fileContent = 'This is the case-insensitive file content.';
+
+ // Mock fs.stat to "find" MyFile.txt when looking for myfile.txt
+ // This simulates a case-insensitive file system or resolution
+ vi.mocked(fsPromises.stat).mockImplementation(async (p) => {
+ if (p.toString().toLowerCase().endsWith('myfile.txt')) {
+ return {
+ isDirectory: () => false,
+ // You might need to add other Stats properties if your code uses them
+ } as Stats;
+ }
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
+ });
+
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${queryPath} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read 1 file.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 134, // New messageId
+ signal: abortController.signal,
+ });
+
+ expect(mockAddItem).toHaveBeenCalledWith(
+ { type: 'user', text: query },
+ 134,
+ );
+ // The atCommandProcessor resolves the path before calling read_many_files.
+ // We expect it to be called with the path that fs.stat "found".
+ // In a real case-insensitive FS, stat(myfile.txt) might return info for MyFile.txt.
+ // The key is that *a* valid path that points to the content is used.
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ // Depending on how path resolution and fs.stat mock interact,
+ // this could be queryPath or actualFilePath.
+ // For this test, we'll assume the processor uses the path that stat "succeeded" with.
+ // If the underlying fs/stat is truly case-insensitive, it might resolve to actualFilePath.
+ // If the mock is simpler, it might use queryPath if stat(queryPath) succeeds.
+ // The most important part is that *some* version of the path that leads to the content is used.
+ // Let's assume it uses the path from the query if stat confirms it exists (even if different case on disk)
+ { paths: [queryPath], respect_git_ignore: true },
+ abortController.signal,
+ );
+ expect(mockAddItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'tool_group',
+ tools: [expect.objectContaining({ status: ToolCallStatus.Success })],
+ }),
+ 134,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `@${queryPath}` }, // Query uses the input path
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${queryPath}:\n` }, // Content display also uses input path
+ { text: fileContent },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ describe('git-aware filtering', () => {
+ it('should skip git-ignored files in @ commands', async () => {
+ const gitIgnoredFile = 'node_modules/package.json';
+ const query = `@${gitIgnoredFile}`;
+
+ // Mock the file discovery service to report this file as git-ignored
+ mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
+ (path: string, options?: { respectGitIgnore?: boolean }) =>
+ path === gitIgnoredFile && options?.respectGitIgnore !== false,
+ );
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 200,
+ signal: abortController.signal,
+ });
+
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ gitIgnoredFile,
+ { respectGitIgnore: true },
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ 'Ignored 1 git-ignored files: node_modules/package.json',
+ );
+ expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
+ expect(result.processedQuery).toEqual([{ text: query }]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should process non-git-ignored files normally', async () => {
+ const validFile = 'src/index.ts';
+ const query = `@${validFile}`;
+ const fileContent = 'console.log("Hello world");';
+
+ mockFileDiscoveryService.shouldIgnoreFile.mockReturnValue(false);
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read 1 file.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 201,
+ signal: abortController.signal,
+ });
+
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ validFile,
+ { respectGitIgnore: true },
+ );
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ { paths: [validFile], respect_git_ignore: true },
+ abortController.signal,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `@${validFile}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${validFile}:\n` },
+ { text: fileContent },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should handle mixed git-ignored and valid files', async () => {
+ const validFile = 'README.md';
+ const gitIgnoredFile = '.env';
+ const query = `@${validFile} @${gitIgnoredFile}`;
+ const fileContent = '# Project README';
+
+ mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
+ (path: string, options?: { respectGitIgnore?: boolean }) =>
+ path === gitIgnoredFile && options?.respectGitIgnore !== false,
+ );
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read 1 file.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 202,
+ signal: abortController.signal,
+ });
+
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ validFile,
+ { respectGitIgnore: true },
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ gitIgnoredFile,
+ { respectGitIgnore: true },
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ 'Ignored 1 git-ignored files: .env',
+ );
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ { paths: [validFile], respect_git_ignore: true },
+ abortController.signal,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `@${validFile} @${gitIgnoredFile}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${validFile}:\n` },
+ { text: fileContent },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should always ignore .git directory files', async () => {
+ const gitFile = '.git/config';
+ const query = `@${gitFile}`;
+
+ mockFileDiscoveryService.shouldIgnoreFile.mockReturnValue(true);
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 203,
+ signal: abortController.signal,
+ });
+
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ gitFile,
+ { respectGitIgnore: true },
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Path ${gitFile} is git-ignored and will be skipped.`,
+ );
+ expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
+ expect(result.processedQuery).toEqual([{ text: query }]);
+ expect(result.shouldProceed).toBe(true);
+ });
+ });
+
+ describe('when recursive file search is disabled', () => {
+ beforeEach(() => {
+ vi.mocked(mockConfig.getEnableRecursiveFileSearch).mockReturnValue(false);
+ });
+
+ it('should not use glob search for a nonexistent file', async () => {
+ const invalidFile = 'nonexistent.txt';
+ const query = `@${invalidFile}`;
+
+ vi.mocked(fsPromises.stat).mockRejectedValue(
+ Object.assign(new Error('ENOENT'), { code: 'ENOENT' }),
+ );
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 300,
+ signal: abortController.signal,
+ });
+
+ expect(mockGlobExecute).not.toHaveBeenCalled();
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Glob tool not found. Path ${invalidFile} will be skipped.`,
+ );
+ expect(result.processedQuery).toEqual([{ text: query }]);
+ expect(result.shouldProceed).toBe(true);
+ });
+ });
+});
diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts
new file mode 100644
index 00000000..486de9f6
--- /dev/null
+++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts
@@ -0,0 +1,423 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import { PartListUnion, PartUnion } from '@google/genai';
+import {
+ Config,
+ getErrorMessage,
+ isNodeError,
+ unescapePath,
+} from '@qwen/qwen-code-core';
+import {
+ HistoryItem,
+ IndividualToolCallDisplay,
+ ToolCallStatus,
+} from '../types.js';
+import { UseHistoryManagerReturn } from './useHistoryManager.js';
+
+interface HandleAtCommandParams {
+ query: string;
+ config: Config;
+ addItem: UseHistoryManagerReturn['addItem'];
+ onDebugMessage: (message: string) => void;
+ messageId: number;
+ signal: AbortSignal;
+}
+
+interface HandleAtCommandResult {
+ processedQuery: PartListUnion | null;
+ shouldProceed: boolean;
+}
+
+interface AtCommandPart {
+ type: 'text' | 'atPath';
+ content: string;
+}
+
+/**
+ * Parses a query string to find all '@' commands and text segments.
+ * Handles \ escaped spaces within paths.
+ */
+function parseAllAtCommands(query: string): AtCommandPart[] {
+ const parts: AtCommandPart[] = [];
+ let currentIndex = 0;
+
+ while (currentIndex < query.length) {
+ let atIndex = -1;
+ let nextSearchIndex = currentIndex;
+ // Find next unescaped '@'
+ while (nextSearchIndex < query.length) {
+ if (
+ query[nextSearchIndex] === '@' &&
+ (nextSearchIndex === 0 || query[nextSearchIndex - 1] !== '\\')
+ ) {
+ atIndex = nextSearchIndex;
+ break;
+ }
+ nextSearchIndex++;
+ }
+
+ if (atIndex === -1) {
+ // No more @
+ if (currentIndex < query.length) {
+ parts.push({ type: 'text', content: query.substring(currentIndex) });
+ }
+ break;
+ }
+
+ // Add text before @
+ if (atIndex > currentIndex) {
+ parts.push({
+ type: 'text',
+ content: query.substring(currentIndex, atIndex),
+ });
+ }
+
+ // Parse @path
+ let pathEndIndex = atIndex + 1;
+ let inEscape = false;
+ while (pathEndIndex < query.length) {
+ const char = query[pathEndIndex];
+ if (inEscape) {
+ inEscape = false;
+ } else if (char === '\\') {
+ inEscape = true;
+ } else if (/\s/.test(char)) {
+ // Path ends at first whitespace not escaped
+ break;
+ }
+ pathEndIndex++;
+ }
+ const rawAtPath = query.substring(atIndex, pathEndIndex);
+ // unescapePath expects the @ symbol to be present, and will handle it.
+ const atPath = unescapePath(rawAtPath);
+ parts.push({ type: 'atPath', content: atPath });
+ currentIndex = pathEndIndex;
+ }
+ // Filter out empty text parts that might result from consecutive @paths or leading/trailing spaces
+ return parts.filter(
+ (part) => !(part.type === 'text' && part.content.trim() === ''),
+ );
+}
+
+/**
+ * Processes user input potentially containing one or more '@' commands.
+ * If found, it attempts to read the specified files/directories using the
+ * 'read_many_files' tool. The user query is modified to include resolved paths,
+ * and the content of the files is appended in a structured block.
+ *
+ * @returns An object indicating whether the main hook should proceed with an
+ * LLM call and the processed query parts (including file content).
+ */
+export async function handleAtCommand({
+ query,
+ config,
+ addItem,
+ onDebugMessage,
+ messageId: userMessageTimestamp,
+ signal,
+}: HandleAtCommandParams): Promise {
+ const commandParts = parseAllAtCommands(query);
+ const atPathCommandParts = commandParts.filter(
+ (part) => part.type === 'atPath',
+ );
+
+ if (atPathCommandParts.length === 0) {
+ addItem({ type: 'user', text: query }, userMessageTimestamp);
+ return { processedQuery: [{ text: query }], shouldProceed: true };
+ }
+
+ addItem({ type: 'user', text: query }, userMessageTimestamp);
+
+ // Get centralized file discovery service
+ const fileDiscovery = config.getFileService();
+ const respectGitIgnore = config.getFileFilteringRespectGitIgnore();
+
+ const pathSpecsToRead: string[] = [];
+ const atPathToResolvedSpecMap = new Map();
+ const contentLabelsForDisplay: string[] = [];
+ const ignoredPaths: string[] = [];
+
+ const toolRegistry = await config.getToolRegistry();
+ const readManyFilesTool = toolRegistry.getTool('read_many_files');
+ const globTool = toolRegistry.getTool('glob');
+
+ if (!readManyFilesTool) {
+ addItem(
+ { type: 'error', text: 'Error: read_many_files tool not found.' },
+ userMessageTimestamp,
+ );
+ return { processedQuery: null, shouldProceed: false };
+ }
+
+ for (const atPathPart of atPathCommandParts) {
+ const originalAtPath = atPathPart.content; // e.g., "@file.txt" or "@"
+
+ if (originalAtPath === '@') {
+ onDebugMessage(
+ 'Lone @ detected, will be treated as text in the modified query.',
+ );
+ continue;
+ }
+
+ const pathName = originalAtPath.substring(1);
+ if (!pathName) {
+ // This case should ideally not be hit if parseAllAtCommands ensures content after @
+ // but as a safeguard:
+ addItem(
+ {
+ type: 'error',
+ text: `Error: Invalid @ command '${originalAtPath}'. No path specified.`,
+ },
+ userMessageTimestamp,
+ );
+ // Decide if this is a fatal error for the whole command or just skip this @ part
+ // For now, let's be strict and fail the command if one @path is malformed.
+ return { processedQuery: null, shouldProceed: false };
+ }
+
+ // Check if path should be ignored based on filtering options
+ if (fileDiscovery.shouldIgnoreFile(pathName, { respectGitIgnore })) {
+ const reason = respectGitIgnore ? 'git-ignored' : 'custom-ignored';
+ onDebugMessage(`Path ${pathName} is ${reason} and will be skipped.`);
+ ignoredPaths.push(pathName);
+ continue;
+ }
+
+ let currentPathSpec = pathName;
+ let resolvedSuccessfully = false;
+
+ try {
+ const absolutePath = path.resolve(config.getTargetDir(), pathName);
+ const stats = await fs.stat(absolutePath);
+ if (stats.isDirectory()) {
+ currentPathSpec = pathName.endsWith('/')
+ ? `${pathName}**`
+ : `${pathName}/**`;
+ onDebugMessage(
+ `Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`,
+ );
+ } else {
+ onDebugMessage(`Path ${pathName} resolved to file: ${currentPathSpec}`);
+ }
+ resolvedSuccessfully = true;
+ } catch (error) {
+ if (isNodeError(error) && error.code === 'ENOENT') {
+ if (config.getEnableRecursiveFileSearch() && globTool) {
+ onDebugMessage(
+ `Path ${pathName} not found directly, attempting glob search.`,
+ );
+ try {
+ const globResult = await globTool.execute(
+ { pattern: `**/*${pathName}*`, path: config.getTargetDir() },
+ signal,
+ );
+ if (
+ globResult.llmContent &&
+ typeof globResult.llmContent === 'string' &&
+ !globResult.llmContent.startsWith('No files found') &&
+ !globResult.llmContent.startsWith('Error:')
+ ) {
+ const lines = globResult.llmContent.split('\n');
+ if (lines.length > 1 && lines[1]) {
+ const firstMatchAbsolute = lines[1].trim();
+ currentPathSpec = path.relative(
+ config.getTargetDir(),
+ firstMatchAbsolute,
+ );
+ onDebugMessage(
+ `Glob search for ${pathName} found ${firstMatchAbsolute}, using relative path: ${currentPathSpec}`,
+ );
+ resolvedSuccessfully = true;
+ } else {
+ onDebugMessage(
+ `Glob search for '**/*${pathName}*' did not return a usable path. Path ${pathName} will be skipped.`,
+ );
+ }
+ } else {
+ onDebugMessage(
+ `Glob search for '**/*${pathName}*' found no files or an error. Path ${pathName} will be skipped.`,
+ );
+ }
+ } catch (globError) {
+ console.error(
+ `Error during glob search for ${pathName}: ${getErrorMessage(globError)}`,
+ );
+ onDebugMessage(
+ `Error during glob search for ${pathName}. Path ${pathName} will be skipped.`,
+ );
+ }
+ } else {
+ onDebugMessage(
+ `Glob tool not found. Path ${pathName} will be skipped.`,
+ );
+ }
+ } else {
+ console.error(
+ `Error stating path ${pathName}: ${getErrorMessage(error)}`,
+ );
+ onDebugMessage(
+ `Error stating path ${pathName}. Path ${pathName} will be skipped.`,
+ );
+ }
+ }
+
+ if (resolvedSuccessfully) {
+ pathSpecsToRead.push(currentPathSpec);
+ atPathToResolvedSpecMap.set(originalAtPath, currentPathSpec);
+ contentLabelsForDisplay.push(pathName);
+ }
+ }
+
+ // Construct the initial part of the query for the LLM
+ let initialQueryText = '';
+ for (let i = 0; i < commandParts.length; i++) {
+ const part = commandParts[i];
+ if (part.type === 'text') {
+ initialQueryText += part.content;
+ } else {
+ // type === 'atPath'
+ const resolvedSpec = atPathToResolvedSpecMap.get(part.content);
+ if (
+ i > 0 &&
+ initialQueryText.length > 0 &&
+ !initialQueryText.endsWith(' ') &&
+ resolvedSpec
+ ) {
+ // Add space if previous part was text and didn't end with space, or if previous was @path
+ const prevPart = commandParts[i - 1];
+ if (
+ prevPart.type === 'text' ||
+ (prevPart.type === 'atPath' &&
+ atPathToResolvedSpecMap.has(prevPart.content))
+ ) {
+ initialQueryText += ' ';
+ }
+ }
+ if (resolvedSpec) {
+ initialQueryText += `@${resolvedSpec}`;
+ } else {
+ // If not resolved for reading (e.g. lone @ or invalid path that was skipped),
+ // add the original @-string back, ensuring spacing if it's not the first element.
+ if (
+ i > 0 &&
+ initialQueryText.length > 0 &&
+ !initialQueryText.endsWith(' ') &&
+ !part.content.startsWith(' ')
+ ) {
+ initialQueryText += ' ';
+ }
+ initialQueryText += part.content;
+ }
+ }
+ }
+ initialQueryText = initialQueryText.trim();
+
+ // Inform user about ignored paths
+ if (ignoredPaths.length > 0) {
+ const ignoreType = respectGitIgnore ? 'git-ignored' : 'custom-ignored';
+ onDebugMessage(
+ `Ignored ${ignoredPaths.length} ${ignoreType} files: ${ignoredPaths.join(', ')}`,
+ );
+ }
+
+ // Fallback for lone "@" or completely invalid @-commands resulting in empty initialQueryText
+ if (pathSpecsToRead.length === 0) {
+ onDebugMessage('No valid file paths found in @ commands to read.');
+ if (initialQueryText === '@' && query.trim() === '@') {
+ // If the only thing was a lone @, pass original query (which might have spaces)
+ return { processedQuery: [{ text: query }], shouldProceed: true };
+ } else if (!initialQueryText && query) {
+ // If all @-commands were invalid and no surrounding text, pass original query
+ return { processedQuery: [{ text: query }], shouldProceed: true };
+ }
+ // Otherwise, proceed with the (potentially modified) query text that doesn't involve file reading
+ return {
+ processedQuery: [{ text: initialQueryText || query }],
+ shouldProceed: true,
+ };
+ }
+
+ const processedQueryParts: PartUnion[] = [{ text: initialQueryText }];
+
+ const toolArgs = {
+ paths: pathSpecsToRead,
+ respect_git_ignore: respectGitIgnore, // Use configuration setting
+ };
+ let toolCallDisplay: IndividualToolCallDisplay;
+
+ try {
+ const result = await readManyFilesTool.execute(toolArgs, signal);
+ toolCallDisplay = {
+ callId: `client-read-${userMessageTimestamp}`,
+ name: readManyFilesTool.displayName,
+ description: readManyFilesTool.getDescription(toolArgs),
+ status: ToolCallStatus.Success,
+ resultDisplay:
+ result.returnDisplay ||
+ `Successfully read: ${contentLabelsForDisplay.join(', ')}`,
+ confirmationDetails: undefined,
+ };
+
+ if (Array.isArray(result.llmContent)) {
+ const fileContentRegex = /^--- (.*?) ---\n\n([\s\S]*?)\n\n$/;
+ processedQueryParts.push({
+ text: '\n--- Content from referenced files ---',
+ });
+ for (const part of result.llmContent) {
+ if (typeof part === 'string') {
+ const match = fileContentRegex.exec(part);
+ if (match) {
+ const filePathSpecInContent = match[1]; // This is a resolved pathSpec
+ const fileActualContent = match[2].trim();
+ processedQueryParts.push({
+ text: `\nContent from @${filePathSpecInContent}:\n`,
+ });
+ processedQueryParts.push({ text: fileActualContent });
+ } else {
+ processedQueryParts.push({ text: part });
+ }
+ } else {
+ // part is a Part object.
+ processedQueryParts.push(part);
+ }
+ }
+ processedQueryParts.push({ text: '\n--- End of content ---' });
+ } else {
+ onDebugMessage(
+ 'read_many_files tool returned no content or empty content.',
+ );
+ }
+
+ addItem(
+ { type: 'tool_group', tools: [toolCallDisplay] } as Omit<
+ HistoryItem,
+ 'id'
+ >,
+ userMessageTimestamp,
+ );
+ return { processedQuery: processedQueryParts, shouldProceed: true };
+ } catch (error: unknown) {
+ toolCallDisplay = {
+ callId: `client-read-${userMessageTimestamp}`,
+ name: readManyFilesTool.displayName,
+ description: readManyFilesTool.getDescription(toolArgs),
+ status: ToolCallStatus.Error,
+ resultDisplay: `Error reading files (${contentLabelsForDisplay.join(', ')}): ${getErrorMessage(error)}`,
+ confirmationDetails: undefined,
+ };
+ addItem(
+ { type: 'tool_group', tools: [toolCallDisplay] } as Omit<
+ HistoryItem,
+ 'id'
+ >,
+ userMessageTimestamp,
+ );
+ return { processedQuery: null, shouldProceed: false };
+ }
+}
diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts
new file mode 100644
index 00000000..4ac6bb24
--- /dev/null
+++ b/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts
@@ -0,0 +1,179 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { act, renderHook } from '@testing-library/react';
+import { vi } from 'vitest';
+import { useShellCommandProcessor } from './shellCommandProcessor';
+import { Config, GeminiClient } from '@qwen/qwen-code-core';
+import * as fs from 'fs';
+import EventEmitter from 'events';
+
+// Mock dependencies
+vi.mock('child_process');
+vi.mock('fs');
+vi.mock('os', () => ({
+ default: {
+ platform: () => 'linux',
+ tmpdir: () => '/tmp',
+ },
+ platform: () => 'linux',
+ tmpdir: () => '/tmp',
+}));
+vi.mock('@qwen/qwen-code-core');
+vi.mock('../utils/textUtils.js', () => ({
+ isBinary: vi.fn(),
+}));
+
+describe('useShellCommandProcessor', () => {
+ let spawnEmitter: EventEmitter;
+ let addItemToHistoryMock: vi.Mock;
+ let setPendingHistoryItemMock: vi.Mock;
+ let onExecMock: vi.Mock;
+ let onDebugMessageMock: vi.Mock;
+ let configMock: Config;
+ let geminiClientMock: GeminiClient;
+
+ beforeEach(async () => {
+ const { spawn } = await import('child_process');
+ spawnEmitter = new EventEmitter();
+ spawnEmitter.stdout = new EventEmitter();
+ spawnEmitter.stderr = new EventEmitter();
+ (spawn as vi.Mock).mockReturnValue(spawnEmitter);
+
+ vi.spyOn(fs, 'existsSync').mockReturnValue(false);
+ vi.spyOn(fs, 'readFileSync').mockReturnValue('');
+ vi.spyOn(fs, 'unlinkSync').mockReturnValue(undefined);
+
+ addItemToHistoryMock = vi.fn();
+ setPendingHistoryItemMock = vi.fn();
+ onExecMock = vi.fn();
+ onDebugMessageMock = vi.fn();
+
+ configMock = {
+ getTargetDir: () => '/test/dir',
+ } as unknown as Config;
+
+ geminiClientMock = {
+ addHistory: vi.fn(),
+ } as unknown as GeminiClient;
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ const renderProcessorHook = () =>
+ renderHook(() =>
+ useShellCommandProcessor(
+ addItemToHistoryMock,
+ setPendingHistoryItemMock,
+ onExecMock,
+ onDebugMessageMock,
+ configMock,
+ geminiClientMock,
+ ),
+ );
+
+ it('should execute a command and update history on success', async () => {
+ const { result } = renderProcessorHook();
+ const abortController = new AbortController();
+
+ act(() => {
+ result.current.handleShellCommand('ls -l', abortController.signal);
+ });
+
+ expect(onExecMock).toHaveBeenCalledTimes(1);
+ const execPromise = onExecMock.mock.calls[0][0];
+
+ // Simulate stdout
+ act(() => {
+ spawnEmitter.stdout.emit('data', Buffer.from('file1.txt\nfile2.txt'));
+ });
+
+ // Simulate process exit
+ act(() => {
+ spawnEmitter.emit('exit', 0, null);
+ });
+
+ await act(async () => {
+ await execPromise;
+ });
+
+ expect(addItemToHistoryMock).toHaveBeenCalledTimes(2);
+ expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({
+ type: 'info',
+ text: 'file1.txt\nfile2.txt',
+ });
+ expect(geminiClientMock.addHistory).toHaveBeenCalledTimes(1);
+ });
+
+ it('should handle binary output', async () => {
+ const { result } = renderProcessorHook();
+ const abortController = new AbortController();
+ const { isBinary } = await import('../utils/textUtils.js');
+ (isBinary as vi.Mock).mockReturnValue(true);
+
+ act(() => {
+ result.current.handleShellCommand(
+ 'cat myimage.png',
+ abortController.signal,
+ );
+ });
+
+ expect(onExecMock).toHaveBeenCalledTimes(1);
+ const execPromise = onExecMock.mock.calls[0][0];
+
+ act(() => {
+ spawnEmitter.stdout.emit('data', Buffer.from([0x89, 0x50, 0x4e, 0x47]));
+ });
+
+ act(() => {
+ spawnEmitter.emit('exit', 0, null);
+ });
+
+ await act(async () => {
+ await execPromise;
+ });
+
+ expect(addItemToHistoryMock).toHaveBeenCalledTimes(2);
+ expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({
+ type: 'info',
+ text: '[Command produced binary output, which is not shown.]',
+ });
+ });
+
+ it('should handle command failure', async () => {
+ const { result } = renderProcessorHook();
+ const abortController = new AbortController();
+
+ act(() => {
+ result.current.handleShellCommand(
+ 'a-bad-command',
+ abortController.signal,
+ );
+ });
+
+ const execPromise = onExecMock.mock.calls[0][0];
+
+ act(() => {
+ spawnEmitter.stderr.emit('data', Buffer.from('command not found'));
+ });
+
+ act(() => {
+ spawnEmitter.emit('exit', 127, null);
+ });
+
+ await act(async () => {
+ await execPromise;
+ });
+
+ expect(addItemToHistoryMock).toHaveBeenCalledTimes(2);
+ expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({
+ type: 'error',
+ text: 'Command exited with code 127.\ncommand not found',
+ });
+ });
+});
diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts
new file mode 100644
index 00000000..a314fb86
--- /dev/null
+++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts
@@ -0,0 +1,348 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { spawn } from 'child_process';
+import { StringDecoder } from 'string_decoder';
+import type { HistoryItemWithoutId } from '../types.js';
+import { useCallback } from 'react';
+import { Config, GeminiClient } from '@qwen/qwen-code-core';
+import { type PartListUnion } from '@google/genai';
+import { formatMemoryUsage } from '../utils/formatters.js';
+import { isBinary } from '../utils/textUtils.js';
+import { UseHistoryManagerReturn } from './useHistoryManager.js';
+import crypto from 'crypto';
+import path from 'path';
+import os from 'os';
+import fs from 'fs';
+import stripAnsi from 'strip-ansi';
+
+const OUTPUT_UPDATE_INTERVAL_MS = 1000;
+const MAX_OUTPUT_LENGTH = 10000;
+
+/**
+ * A structured result from a shell command execution.
+ */
+interface ShellExecutionResult {
+ rawOutput: Buffer;
+ output: string;
+ exitCode: number | null;
+ signal: NodeJS.Signals | null;
+ error: Error | null;
+ aborted: boolean;
+}
+
+/**
+ * Executes a shell command using `spawn`, capturing all output and lifecycle events.
+ * This is the single, unified implementation for shell execution.
+ *
+ * @param commandToExecute The exact command string to run.
+ * @param cwd The working directory to execute the command in.
+ * @param abortSignal An AbortSignal to terminate the process.
+ * @param onOutputChunk A callback for streaming real-time output.
+ * @param onDebugMessage A callback for logging debug information.
+ * @returns A promise that resolves with the complete execution result.
+ */
+function executeShellCommand(
+ commandToExecute: string,
+ cwd: string,
+ abortSignal: AbortSignal,
+ onOutputChunk: (chunk: string) => void,
+ onDebugMessage: (message: string) => void,
+): Promise {
+ return new Promise((resolve) => {
+ const isWindows = os.platform() === 'win32';
+ const shell = isWindows ? 'cmd.exe' : 'bash';
+ const shellArgs = isWindows
+ ? ['/c', commandToExecute]
+ : ['-c', commandToExecute];
+
+ const child = spawn(shell, shellArgs, {
+ cwd,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ detached: !isWindows, // Use process groups on non-Windows for robust killing
+ });
+
+ // Use decoders to handle multi-byte characters safely (for streaming output).
+ const stdoutDecoder = new StringDecoder('utf8');
+ const stderrDecoder = new StringDecoder('utf8');
+
+ let stdout = '';
+ let stderr = '';
+ const outputChunks: Buffer[] = [];
+ let error: Error | null = null;
+ let exited = false;
+
+ let streamToUi = true;
+ const MAX_SNIFF_SIZE = 4096;
+ let sniffedBytes = 0;
+
+ const handleOutput = (data: Buffer, stream: 'stdout' | 'stderr') => {
+ outputChunks.push(data);
+
+ if (streamToUi && sniffedBytes < MAX_SNIFF_SIZE) {
+ // Use a limited-size buffer for the check to avoid performance issues.
+ const sniffBuffer = Buffer.concat(outputChunks.slice(0, 20));
+ sniffedBytes = sniffBuffer.length;
+
+ if (isBinary(sniffBuffer)) {
+ streamToUi = false;
+ // Overwrite any garbled text that may have streamed with a clear message.
+ onOutputChunk('[Binary output detected. Halting stream...]');
+ }
+ }
+
+ const decodedChunk =
+ stream === 'stdout'
+ ? stdoutDecoder.write(data)
+ : stderrDecoder.write(data);
+ if (stream === 'stdout') {
+ stdout += stripAnsi(decodedChunk);
+ } else {
+ stderr += stripAnsi(decodedChunk);
+ }
+
+ if (!exited && streamToUi) {
+ // Send only the new chunk to avoid re-rendering the whole output.
+ const combinedOutput = stdout + (stderr ? `\n${stderr}` : '');
+ onOutputChunk(combinedOutput);
+ } else if (!exited && !streamToUi) {
+ // Send progress updates for the binary stream
+ const totalBytes = outputChunks.reduce(
+ (sum, chunk) => sum + chunk.length,
+ 0,
+ );
+ onOutputChunk(
+ `[Receiving binary output... ${formatMemoryUsage(totalBytes)} received]`,
+ );
+ }
+ };
+
+ child.stdout.on('data', (data) => handleOutput(data, 'stdout'));
+ child.stderr.on('data', (data) => handleOutput(data, 'stderr'));
+ child.on('error', (err) => {
+ error = err;
+ });
+
+ const abortHandler = async () => {
+ if (child.pid && !exited) {
+ onDebugMessage(`Aborting shell command (PID: ${child.pid})`);
+ if (isWindows) {
+ spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t']);
+ } else {
+ try {
+ // Kill the entire process group (negative PID).
+ // SIGTERM first, then SIGKILL if it doesn't die.
+ process.kill(-child.pid, 'SIGTERM');
+ await new Promise((res) => setTimeout(res, 200));
+ if (!exited) {
+ process.kill(-child.pid, 'SIGKILL');
+ }
+ } catch (_e) {
+ // Fallback to killing just the main process if group kill fails.
+ if (!exited) child.kill('SIGKILL');
+ }
+ }
+ }
+ };
+
+ abortSignal.addEventListener('abort', abortHandler, { once: true });
+
+ child.on('exit', (code, signal) => {
+ exited = true;
+ abortSignal.removeEventListener('abort', abortHandler);
+
+ // Handle any final bytes lingering in the decoders
+ stdout += stdoutDecoder.end();
+ stderr += stderrDecoder.end();
+
+ const finalBuffer = Buffer.concat(outputChunks);
+
+ resolve({
+ rawOutput: finalBuffer,
+ output: stdout + (stderr ? `\n${stderr}` : ''),
+ exitCode: code,
+ signal,
+ error,
+ aborted: abortSignal.aborted,
+ });
+ });
+ });
+}
+
+function addShellCommandToGeminiHistory(
+ geminiClient: GeminiClient,
+ rawQuery: string,
+ resultText: string,
+) {
+ const modelContent =
+ resultText.length > MAX_OUTPUT_LENGTH
+ ? resultText.substring(0, MAX_OUTPUT_LENGTH) + '\n... (truncated)'
+ : resultText;
+
+ geminiClient.addHistory({
+ role: 'user',
+ parts: [
+ {
+ text: `I ran the following shell command:
+\`\`\`sh
+${rawQuery}
+\`\`\`
+
+This produced the following result:
+\`\`\`
+${modelContent}
+\`\`\``,
+ },
+ ],
+ });
+}
+
+/**
+ * Hook to process shell commands.
+ * Orchestrates command execution and updates history and agent context.
+ */
+export const useShellCommandProcessor = (
+ addItemToHistory: UseHistoryManagerReturn['addItem'],
+ setPendingHistoryItem: React.Dispatch<
+ React.SetStateAction
+ >,
+ onExec: (command: Promise) => void,
+ onDebugMessage: (message: string) => void,
+ config: Config,
+ geminiClient: GeminiClient,
+) => {
+ const handleShellCommand = useCallback(
+ (rawQuery: PartListUnion, abortSignal: AbortSignal): boolean => {
+ if (typeof rawQuery !== 'string' || rawQuery.trim() === '') {
+ return false;
+ }
+
+ const userMessageTimestamp = Date.now();
+ addItemToHistory(
+ { type: 'user_shell', text: rawQuery },
+ userMessageTimestamp,
+ );
+
+ const isWindows = os.platform() === 'win32';
+ const targetDir = config.getTargetDir();
+ 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 execPromise = new Promise((resolve) => {
+ let lastUpdateTime = 0;
+
+ onDebugMessage(`Executing in ${targetDir}: ${commandToExecute}`);
+ executeShellCommand(
+ commandToExecute,
+ targetDir,
+ abortSignal,
+ (streamedOutput) => {
+ // Throttle pending UI updates to avoid excessive re-renders.
+ if (Date.now() - lastUpdateTime > OUTPUT_UPDATE_INTERVAL_MS) {
+ setPendingHistoryItem({ type: 'info', text: streamedOutput });
+ lastUpdateTime = Date.now();
+ }
+ },
+ onDebugMessage,
+ )
+ .then((result) => {
+ // TODO(abhipatel12) - Consider updating pending item and using timeout to ensure
+ // there is no jump where intermediate output is skipped.
+ setPendingHistoryItem(null);
+
+ let historyItemType: HistoryItemWithoutId['type'] = 'info';
+ let mainContent: string;
+
+ // The context sent to the model utilizes a text tokenizer which means raw binary data is
+ // cannot be parsed and understood and thus would only pollute the context window and waste
+ // tokens.
+ if (isBinary(result.rawOutput)) {
+ mainContent =
+ '[Command produced binary output, which is not shown.]';
+ } else {
+ mainContent =
+ result.output.trim() || '(Command produced no output)';
+ }
+
+ let finalOutput = mainContent;
+
+ if (result.error) {
+ historyItemType = 'error';
+ finalOutput = `${result.error.message}\n${finalOutput}`;
+ } else if (result.aborted) {
+ finalOutput = `Command was cancelled.\n${finalOutput}`;
+ } else if (result.signal) {
+ historyItemType = 'error';
+ finalOutput = `Command terminated by signal: ${result.signal}.\n${finalOutput}`;
+ } else if (result.exitCode !== 0) {
+ historyItemType = 'error';
+ finalOutput = `Command exited with code ${result.exitCode}.\n${finalOutput}`;
+ }
+
+ if (pwdFilePath && fs.existsSync(pwdFilePath)) {
+ const finalPwd = fs.readFileSync(pwdFilePath, 'utf8').trim();
+ if (finalPwd && finalPwd !== targetDir) {
+ const warning = `WARNING: shell mode is stateless; the directory change to '${finalPwd}' will not persist.`;
+ finalOutput = `${warning}\n\n${finalOutput}`;
+ }
+ }
+
+ // Add the complete, contextual result to the local UI history.
+ addItemToHistory(
+ { type: historyItemType, text: finalOutput },
+ userMessageTimestamp,
+ );
+
+ // Add the same complete, contextual result to the LLM's history.
+ addShellCommandToGeminiHistory(geminiClient, rawQuery, finalOutput);
+ })
+ .catch((err) => {
+ setPendingHistoryItem(null);
+ const errorMessage =
+ err instanceof Error ? err.message : String(err);
+ addItemToHistory(
+ {
+ type: 'error',
+ text: `An unexpected error occurred: ${errorMessage}`,
+ },
+ userMessageTimestamp,
+ );
+ })
+ .finally(() => {
+ if (pwdFilePath && fs.existsSync(pwdFilePath)) {
+ fs.unlinkSync(pwdFilePath);
+ }
+ resolve();
+ });
+ });
+
+ onExec(execPromise);
+ return true; // Command was initiated
+ },
+ [
+ config,
+ onDebugMessage,
+ addItemToHistory,
+ setPendingHistoryItem,
+ onExec,
+ geminiClient,
+ ],
+ );
+
+ return { handleShellCommand };
+};
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
new file mode 100644
index 00000000..ae9f836f
--- /dev/null
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
@@ -0,0 +1,1301 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+const { mockProcessExit } = vi.hoisted(() => ({
+ mockProcessExit: vi.fn((_code?: number): never => undefined as never),
+}));
+
+vi.mock('node:process', () => ({
+ default: {
+ exit: mockProcessExit,
+ cwd: vi.fn(() => '/mock/cwd'),
+ get env() {
+ return process.env;
+ }, // Use a getter to ensure current process.env is used
+ platform: 'test-platform',
+ version: 'test-node-version',
+ memoryUsage: vi.fn(() => ({
+ rss: 12345678,
+ heapTotal: 23456789,
+ heapUsed: 10234567,
+ external: 1234567,
+ arrayBuffers: 123456,
+ })),
+ },
+ // Provide top-level exports as well for compatibility
+ exit: mockProcessExit,
+ cwd: vi.fn(() => '/mock/cwd'),
+ get env() {
+ return process.env;
+ }, // Use a getter here too
+ platform: 'test-platform',
+ version: 'test-node-version',
+ memoryUsage: vi.fn(() => ({
+ rss: 12345678,
+ heapTotal: 23456789,
+ heapUsed: 10234567,
+ external: 1234567,
+ arrayBuffers: 123456,
+ })),
+}));
+
+vi.mock('node:fs/promises', () => ({
+ readFile: vi.fn(),
+ writeFile: vi.fn(),
+ mkdir: vi.fn(),
+}));
+
+const mockGetCliVersionFn = vi.fn(() => Promise.resolve('0.1.0'));
+vi.mock('../../utils/version.js', () => ({
+ getCliVersion: (...args: []) => mockGetCliVersionFn(...args),
+}));
+
+import { act, renderHook } from '@testing-library/react';
+import { vi, describe, it, expect, beforeEach, afterEach, Mock } from 'vitest';
+import open from 'open';
+import { useSlashCommandProcessor } from './slashCommandProcessor.js';
+import { MessageType, SlashCommandProcessorResult } from '../types.js';
+import {
+ Config,
+ MCPDiscoveryState,
+ MCPServerStatus,
+ getMCPDiscoveryState,
+ getMCPServerStatus,
+ GeminiClient,
+} from '@qwen/qwen-code-core';
+import { useSessionStats } from '../contexts/SessionContext.js';
+import { LoadedSettings } from '../../config/settings.js';
+import * as ShowMemoryCommandModule from './useShowMemoryCommand.js';
+import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
+import { CommandService } from '../../services/CommandService.js';
+import { SlashCommand } from '../commands/types.js';
+
+vi.mock('../contexts/SessionContext.js', () => ({
+ useSessionStats: vi.fn(),
+}));
+
+vi.mock('../../services/CommandService.js');
+
+vi.mock('./useShowMemoryCommand.js', () => ({
+ SHOW_MEMORY_COMMAND_NAME: '/memory show',
+ createShowMemoryAction: vi.fn(() => vi.fn()),
+}));
+
+vi.mock('open', () => ({
+ default: vi.fn(),
+}));
+
+vi.mock('@qwen/qwen-code-core', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ getMCPServerStatus: vi.fn(),
+ getMCPDiscoveryState: vi.fn(),
+ };
+});
+
+describe('useSlashCommandProcessor', () => {
+ let mockAddItem: ReturnType;
+ let mockClearItems: ReturnType;
+ let mockLoadHistory: ReturnType;
+ let mockRefreshStatic: ReturnType;
+ let mockSetShowHelp: ReturnType;
+ let mockOnDebugMessage: ReturnType;
+ let mockOpenThemeDialog: ReturnType;
+ let mockOpenAuthDialog: ReturnType;
+ let mockOpenEditorDialog: ReturnType;
+ let mockSetQuittingMessages: ReturnType;
+ let mockTryCompressChat: ReturnType;
+ let mockGeminiClient: GeminiClient;
+ let mockConfig: Config;
+ let mockCorgiMode: ReturnType