mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-03 21:51:11 -07:00
Compare commits
169 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83a3b07050 | |||
| 18e854c330 | |||
| 29d4b1e6b8 | |||
| 2a8e1a8cc1 | |||
| c9dbf70043 | |||
| eb88343419 | |||
| 217f277580 | |||
| 02e68e4554 | |||
| 982fd1fc29 | |||
| 6289c3ee3f | |||
| b1f7a7e6f7 | |||
| ea0e3de430 | |||
| f6a5fa0e03 | |||
| de86bccd0d | |||
| 46079d9daa | |||
| 1d5e792a41 | |||
| cd7a5c9604 | |||
| bb6c574144 | |||
| 66e7b479ae | |||
| 8030404b08 | |||
| 933bc5774f | |||
| 4afd3741df | |||
| 428e602882 | |||
| 04f65d7b4e | |||
| eda47f587c | |||
| 8dbaa2bcea | |||
| 778de55fd8 | |||
| c7c409c68f | |||
| d66ec38f82 | |||
| 63c918fe7d | |||
| b2e866585d | |||
| b518125c46 | |||
| e8be252b75 | |||
| 92e31e3c4a | |||
| e931ebe581 | |||
| 91fcca3b1c | |||
| aa52462550 | |||
| 0f7a136612 | |||
| 8faa23cea6 | |||
| a6dca02344 | |||
| 304caa4e43 | |||
| 7bbfaabffa | |||
| 6adae9f775 | |||
| b81fe68325 | |||
| 2fc61685a3 | |||
| c572b9e9ac | |||
| 8d3e93cdb0 | |||
| 548641c952 | |||
| 6ef2a92233 | |||
| e9c9dd1d67 | |||
| ca6786a28b | |||
| 2e8c6cfdbb | |||
| 95d9a33996 | |||
| b8cc414d5b | |||
| d7bff8610f | |||
| e049d5e4e8 | |||
| 8437ce940a | |||
| 7d92242011 | |||
| d65eab01d2 | |||
| 2306e60be4 | |||
| 3b678a4da0 | |||
| 64cde8d439 | |||
| 0167392f22 | |||
| 15891721ad | |||
| 8a2e0fac0d | |||
| 8656ce8a27 | |||
| ed7bcf9968 | |||
| 465ec9759d | |||
| 950244f6b0 | |||
| 64c75cb767 | |||
| 7b7f2fc69e | |||
| d315f4d3da | |||
| 9703fe73cf | |||
| 93b57b82c1 | |||
| 0e955da171 | |||
| 39b3f20a22 | |||
| b9762a3ee1 | |||
| 33e3ed0f6c | |||
| 446058cb1c | |||
| d130d99ff0 | |||
| 72dae7e0ee | |||
| 6f7d798189 | |||
| 3090008b1c | |||
| 461c277bf2 | |||
| b54e688c75 | |||
| b08b0d715b | |||
| c87d1aed4c | |||
| 356f76e545 | |||
| 1fb55dcb2e | |||
| d74bf9ef2f | |||
| e04a5f0cb0 | |||
| ea7393f7fd | |||
| c7d17dda49 | |||
| 9d187e041c | |||
| 14f0cb4538 | |||
| ca48661423 | |||
| 041463d112 | |||
| c1401682ed | |||
| 8bc3cfe29a | |||
| 77e226c55f | |||
| b9f8858bfb | |||
| f7b97ef55e | |||
| 88f1ec8d0a | |||
| aa480e5fbb | |||
| 41e627a7ee | |||
| e1e3efc9d0 | |||
| 18dd399cb5 | |||
| f1ca7fa40a | |||
| 6166d7f6ec | |||
| ffb80c2426 | |||
| 4ab1b9895a | |||
| 84710b1953 | |||
| a7f758eb3a | |||
| 26505b580c | |||
| e51f3e11f1 | |||
| d75792703a | |||
| e5f7a9c424 | |||
| 9062a943e7 | |||
| 7e02ef697d | |||
| 41a8809280 | |||
| 01d2d43737 | |||
| 16da6918cb | |||
| fbfad06307 | |||
| f8138262fa | |||
| 1a4ae41397 | |||
| 75bc41fc20 | |||
| ced5110dab | |||
| 76d020511f | |||
| 02cf264ee1 | |||
| eb3f3cfdb8 | |||
| 660368f249 | |||
| 97ad3d97cb | |||
| cf021ccae4 | |||
| eb75f59a96 | |||
| 030847a80a | |||
| 722c4933dc | |||
| 3e2f4eb8ba | |||
| aca6bf6aa0 | |||
| d48c934357 | |||
| 41cc6cf105 | |||
| dd04b46e86 | |||
| 1bd4f9d8b6 | |||
| 1aa35c8796 | |||
| 51d3f44d51 | |||
| d4b418ba01 | |||
| bd77515fd9 | |||
| 1c77bac146 | |||
| 0be8b5b1ed | |||
| d5996fea99 | |||
| a1dd19738e | |||
| 17b3eb730a | |||
| 4c961df313 | |||
| 19bdd95eab | |||
| 143bb63483 | |||
| c64b5ec4a3 | |||
| 57012ae5b3 | |||
| db99beda36 | |||
| 97b31c4eef | |||
| d1eb87c81f | |||
| 8f9bb6bccc | |||
| 416d243027 | |||
| 5fe5d1da46 | |||
| 2d683bb6f8 | |||
| 7956eb239e | |||
| a26463b056 | |||
| 982eee63b6 | |||
| b54215f0a5 | |||
| 521dc7f26c | |||
| da85e3f8f2 |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"experimental": {
|
||||
"skills": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: pr-creator
|
||||
description:
|
||||
Use this skill when asked to create a pull request (PR). It ensures all PRs
|
||||
follow the repository's established templates and standards.
|
||||
---
|
||||
|
||||
# Pull Request Creator
|
||||
|
||||
This skill guides the creation of high-quality Pull Requests that adhere to the
|
||||
repository's standards.
|
||||
|
||||
## Workflow
|
||||
|
||||
Follow these steps to create a Pull Request:
|
||||
|
||||
1. **Locate Template**: Search for a pull request template in the repository.
|
||||
- Check `.github/pull_request_template.md`
|
||||
- Check `.github/PULL_REQUEST_TEMPLATE.md`
|
||||
- If multiple templates exist (e.g., in `.github/PULL_REQUEST_TEMPLATE/`),
|
||||
ask the user which one to use or select the most appropriate one based on
|
||||
the context (e.g., `bug_fix.md` vs `feature.md`).
|
||||
|
||||
2. **Read Template**: Read the content of the identified template file.
|
||||
|
||||
3. **Draft Description**: Create a PR description that strictly follows the
|
||||
template's structure.
|
||||
- **Headings**: Keep all headings from the template.
|
||||
- **Checklists**: Review each item. Mark with `[x]` if completed. If an item
|
||||
is not applicable, leave it unchecked or mark as `[ ]` (depending on the
|
||||
template's instructions) or remove it if the template allows flexibility
|
||||
(but prefer keeping it unchecked for transparency).
|
||||
- **Content**: Fill in the sections with clear, concise summaries of your
|
||||
changes.
|
||||
- **Related Issues**: Link any issues fixed or related to this PR (e.g.,
|
||||
"Fixes #123").
|
||||
|
||||
4. **Create PR**: Use the `gh` CLI to create the PR.
|
||||
```bash
|
||||
gh pr create --title "type(scope): succinct description" --body "..."
|
||||
```
|
||||
- **Title**: Ensure the title follows the
|
||||
[Conventional Commits](https://www.conventionalcommits.org/) format if the
|
||||
repository uses it (e.g., `feat(ui): add new button`,
|
||||
`fix(core): resolve crash`).
|
||||
|
||||
## Principles
|
||||
|
||||
- **Compliance**: Never ignore the PR template. It exists for a reason.
|
||||
- **Completeness**: Fill out all relevant sections.
|
||||
- **Accuracy**: Don't check boxes for tasks you haven't done.
|
||||
+144
-108
@@ -4,134 +4,170 @@ 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() {
|
||||
if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
|
||||
echo "‼️ Missing \$GITHUB_REPOSITORY - this must be run from GitHub Actions"
|
||||
return 1
|
||||
# Global cache for issue labels (compatible with Bash 3.2)
|
||||
# Stores "ISSUE_NUM:LABELS" pairs separated by spaces
|
||||
ISSUE_LABELS_CACHE_FLAT=""
|
||||
|
||||
# Function to get area and priority labels from an issue (with caching)
|
||||
get_issue_labels() {
|
||||
local ISSUE_NUM=$1
|
||||
if [[ -z "${ISSUE_NUM}" || "${ISSUE_NUM}" == "null" || "${ISSUE_NUM}" == "" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -z "${GITHUB_OUTPUT:-}" ]]; then
|
||||
echo "‼️ Missing \$GITHUB_OUTPUT - this must be run from GitHub Actions"
|
||||
return 1
|
||||
# Check cache
|
||||
case " ${ISSUE_LABELS_CACHE_FLAT} " in
|
||||
*" ${ISSUE_NUM}:"*)
|
||||
local suffix="${ISSUE_LABELS_CACHE_FLAT#* ${ISSUE_NUM}:}"
|
||||
echo "${suffix%% *}"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
echo " 📥 Fetching area and priority labels from issue #${ISSUE_NUM}" >&2
|
||||
local gh_output
|
||||
if ! gh_output=$(gh issue view "${ISSUE_NUM}" --repo "${GITHUB_REPOSITORY}" --json labels -q '.labels[].name' 2>/dev/null); then
|
||||
echo " ⚠️ Could not fetch issue #${ISSUE_NUM}" >&2
|
||||
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT} ${ISSUE_NUM}:"
|
||||
return
|
||||
fi
|
||||
|
||||
local labels
|
||||
labels=$(echo "${gh_output}" | grep -E "^(area|priority)/" | tr '\n' ',' | sed 's/,$//' || echo "")
|
||||
|
||||
# Save to flat cache
|
||||
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT} ${ISSUE_NUM}:${labels}"
|
||||
echo "$labels"
|
||||
}
|
||||
|
||||
# Function to process a single PR with pre-fetched data
|
||||
process_pr_optimized() {
|
||||
local PR_NUMBER=$1
|
||||
local IS_DRAFT=$2
|
||||
local ISSUE_NUMBER=$3
|
||||
local CURRENT_LABELS=$4 # Comma-separated labels
|
||||
|
||||
echo "🔄 Processing PR #${PR_NUMBER}"
|
||||
|
||||
# Get closing issue number with error handling
|
||||
local ISSUE_NUMBER
|
||||
if ! ISSUE_NUMBER=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json closingIssuesReferences -q '.closingIssuesReferences.nodes[0].number' 2>/dev/null); then
|
||||
echo " ⚠️ Could not fetch closing issue for PR #${PR_NUMBER}"
|
||||
local LABELS_TO_ADD=""
|
||||
local LABELS_TO_REMOVE=""
|
||||
|
||||
if [[ -z "${ISSUE_NUMBER}" || "${ISSUE_NUMBER}" == "null" || "${ISSUE_NUMBER}" == "" ]]; then
|
||||
if [[ "${IS_DRAFT}" == "true" ]]; then
|
||||
echo " 📝 PR #${PR_NUMBER} is a draft and has no linked issue"
|
||||
if [[ ",${CURRENT_LABELS}," == ",status/need-issue,"* ]]; then
|
||||
echo " ➖ Removing status/need-issue label"
|
||||
LABELS_TO_REMOVE="status/need-issue"
|
||||
fi
|
||||
else
|
||||
echo " ⚠️ No linked issue found for PR #${PR_NUMBER}"
|
||||
if [[ ",${CURRENT_LABELS}," != ",status/need-issue,"* ]]; then
|
||||
echo " ➕ Adding status/need-issue label"
|
||||
LABELS_TO_ADD="status/need-issue"
|
||||
fi
|
||||
|
||||
if [[ -z "${PRS_NEEDING_COMMENT}" ]]; then
|
||||
PRS_NEEDING_COMMENT="${PR_NUMBER}"
|
||||
else
|
||||
PRS_NEEDING_COMMENT="${PRS_NEEDING_COMMENT},${PR_NUMBER}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " 🔗 Found linked issue #${ISSUE_NUMBER}"
|
||||
|
||||
if [[ ",${CURRENT_LABELS}," == ",status/need-issue,"* ]]; then
|
||||
echo " ➖ Removing status/need-issue label"
|
||||
LABELS_TO_REMOVE="status/need-issue"
|
||||
fi
|
||||
|
||||
local ISSUE_LABELS
|
||||
ISSUE_LABELS=$(get_issue_labels "${ISSUE_NUMBER}")
|
||||
|
||||
if [[ -n "${ISSUE_LABELS}" ]]; then
|
||||
local IFS_OLD=$IFS
|
||||
IFS=','
|
||||
for label in ${ISSUE_LABELS}; do
|
||||
if [[ -n "${label}" ]] && [[ ",${CURRENT_LABELS}," != *",${label},"* ]]; then
|
||||
if [[ -z "${LABELS_TO_ADD}" ]]; then
|
||||
LABELS_TO_ADD="${label}"
|
||||
else
|
||||
LABELS_TO_ADD="${LABELS_TO_ADD},${label}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
IFS=$IFS_OLD
|
||||
fi
|
||||
|
||||
if [[ -z "${LABELS_TO_ADD}" && -z "${LABELS_TO_REMOVE}" ]]; then
|
||||
echo " ✅ Labels already synchronized"
|
||||
fi
|
||||
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
|
||||
|
||||
# Apply label changes
|
||||
if [[ -n "${LABELS_TO_ADD}" || -n "${LABELS_TO_REMOVE}" ]]; then
|
||||
local EDIT_CMD=("gh" "pr" "edit" "${PR_NUMBER}" "--repo" "${GITHUB_REPOSITORY}")
|
||||
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
|
||||
echo " ➕ Syncing labels to add: ${LABELS_TO_ADD}"
|
||||
EDIT_CMD+=("--add-label" "${LABELS_TO_ADD}")
|
||||
fi
|
||||
|
||||
if [[ -z "${LABELS_TO_ADD}" ]]; then
|
||||
echo "✅ Labels already synchronized"
|
||||
if [[ -n "${LABELS_TO_REMOVE}" ]]; then
|
||||
echo " ➖ Syncing labels to remove: ${LABELS_TO_REMOVE}"
|
||||
EDIT_CMD+=("--remove-label" "${LABELS_TO_REMOVE}")
|
||||
fi
|
||||
echo "needs_comment=false" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
("${EDIT_CMD[@]}" 2>/dev/null || true)
|
||||
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
|
||||
if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
|
||||
echo "‼️ Missing \$GITHUB_REPOSITORY - this must be run from GitHub Actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${GITHUB_OUTPUT:-}" ]]; then
|
||||
echo "‼️ Missing \$GITHUB_OUTPUT - this must be run from GitHub Actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JQ_EXTRACT_FIELDS='{
|
||||
number: .number,
|
||||
isDraft: .isDraft,
|
||||
issue: (.closingIssuesReferences[0].number // (.body // "" | capture("(^|[^a-zA-Z0-9])#(?<num>[0-9]+)([^a-zA-Z0-9]|$)")? | .num) // "null"),
|
||||
labels: [.labels[].name] | join(",")
|
||||
}'
|
||||
|
||||
JQ_TSV_FORMAT='"\((.number | tostring))\t\(.isDraft)\t\((.issue // "null") | tostring)\t\(.labels)"'
|
||||
|
||||
if [[ -n "${PR_NUMBER:-}" ]]; then
|
||||
echo "🔄 Processing single PR #${PR_NUMBER}"
|
||||
PR_DATA=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json number,closingIssuesReferences,isDraft,body,labels 2>/dev/null) || {
|
||||
echo "❌ Failed to fetch data for PR #${PR_NUMBER}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
line=$(echo "$PR_DATA" | jq -r "$JQ_EXTRACT_FIELDS | $JQ_TSV_FORMAT")
|
||||
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "$line"
|
||||
process_pr_optimized "$pr_num" "$is_draft" "$issue_num" "$current_labels"
|
||||
else
|
||||
echo "📥 Getting all open pull requests..."
|
||||
PR_DATA_ALL=$(gh pr list --repo "${GITHUB_REPOSITORY}" --state open --limit 1000 --json number,closingIssuesReferences,isDraft,body,labels 2>/dev/null) || {
|
||||
echo "❌ Failed to fetch PR list"
|
||||
exit 1
|
||||
}
|
||||
|
||||
PR_COUNT=$(echo "${PR_DATA_ALL}" | jq '. | length')
|
||||
echo "📊 Found ${PR_COUNT} open PRs to process"
|
||||
|
||||
while read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "$line"
|
||||
process_pr_optimized "$pr_num" "$is_draft" "$issue_num" "$current_labels"
|
||||
done < <(echo "${PR_DATA_ALL}" | jq -r ".[] | $JQ_EXTRACT_FIELDS | $JQ_TSV_FORMAT")
|
||||
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"
|
||||
echo "✅ PR triage completed"
|
||||
@@ -168,6 +168,7 @@ jobs:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
VERBOSE: 'true'
|
||||
BUILD_SANDBOX_FLAGS: '--cache-from type=gha --cache-to type=gha,mode=max'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
|
||||
@@ -276,6 +277,37 @@ jobs:
|
||||
shell: 'pwsh'
|
||||
run: 'npm run test:integration:sandbox:none'
|
||||
|
||||
evals:
|
||||
name: 'Evals (ALWAYS_PASSING)'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: |
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Run Evals (Required to pass)'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
run: 'npm run test:always_passing_evals'
|
||||
|
||||
e2e:
|
||||
name: 'E2E'
|
||||
if: |
|
||||
@@ -283,13 +315,15 @@ jobs:
|
||||
needs:
|
||||
- 'e2e_linux'
|
||||
- 'e2e_mac'
|
||||
- 'evals'
|
||||
- 'merge_queue_skipper'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Check E2E test results'
|
||||
run: |
|
||||
if [[ ${{ needs.e2e_linux.result }} != 'success' || \
|
||||
${{ needs.e2e_mac.result }} != 'success' ]]; then
|
||||
${{ needs.e2e_mac.result }} != 'success' || \
|
||||
${{ needs.evals.result }} != 'success' ]]; then
|
||||
echo "One or more E2E jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+44
-14
@@ -50,6 +50,8 @@ jobs:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
env:
|
||||
GEMINI_LINT_TEMP_DIR: '${{ github.workspace }}/.gemini-linters'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
@@ -63,9 +65,21 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Cache Linters'
|
||||
uses: 'actions/cache@v4'
|
||||
with:
|
||||
path: '${{ env.GEMINI_LINT_TEMP_DIR }}'
|
||||
key: "${{ runner.os }}-${{ runner.arch }}-linters-${{ hashFiles('scripts/lint.js') }}"
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Cache ESLint'
|
||||
uses: 'actions/cache@v4'
|
||||
with:
|
||||
path: '.eslintcache'
|
||||
key: "${{ runner.os }}-eslint-${{ hashFiles('package-lock.json', 'eslint.config.js') }}"
|
||||
|
||||
- name: 'Validate NOTICES.txt'
|
||||
run: 'git diff --exit-code packages/vscode-ide-companion/NOTICES.txt'
|
||||
|
||||
@@ -111,10 +125,9 @@ jobs:
|
||||
args: '--verbose --accept 200,503 ./**/*.md'
|
||||
fail: true
|
||||
test_linux:
|
||||
name: 'Test (Linux)'
|
||||
name: 'Test (Linux) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs:
|
||||
- 'lint'
|
||||
- 'merge_queue_skipper'
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
permissions:
|
||||
@@ -127,6 +140,9 @@ jobs:
|
||||
- '20.x'
|
||||
- '22.x'
|
||||
- '24.x'
|
||||
shard:
|
||||
- 'cli'
|
||||
- 'others'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
@@ -146,7 +162,14 @@ jobs:
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
NO_COLOR: true
|
||||
run: 'npm run test:ci'
|
||||
run: |
|
||||
if [[ "${{ matrix.shard }}" == "cli" ]]; then
|
||||
npm run test:ci --workspace @google/gemini-cli
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
- name: 'Bundle'
|
||||
run: 'npm run bundle'
|
||||
@@ -162,7 +185,7 @@ jobs:
|
||||
${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
|
||||
with:
|
||||
name: 'Test Results (Node ${{ matrix.node-version }})'
|
||||
name: 'Test Results (Node ${{ runner.os }}, ${{ matrix.node-version }}, ${{ matrix.shard }})'
|
||||
path: 'packages/*/junit.xml'
|
||||
reporter: 'java-junit'
|
||||
fail-on-error: 'false'
|
||||
@@ -172,14 +195,13 @@ jobs:
|
||||
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'test-results-fork-${{ matrix.node-version }}-${{ runner.os }}'
|
||||
name: 'test-results-fork-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
|
||||
path: 'packages/*/junit.xml'
|
||||
|
||||
test_mac:
|
||||
name: 'Test (Mac)'
|
||||
runs-on: '${{ matrix.os }}'
|
||||
name: 'Test (Mac) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
|
||||
runs-on: 'macos-latest'
|
||||
needs:
|
||||
- 'lint'
|
||||
- 'merge_queue_skipper'
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
permissions:
|
||||
@@ -189,12 +211,13 @@ jobs:
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- 'macos-latest'
|
||||
node-version:
|
||||
- '20.x'
|
||||
- '22.x'
|
||||
- '24.x'
|
||||
shard:
|
||||
- 'cli'
|
||||
- 'others'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
@@ -214,7 +237,14 @@ jobs:
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
NO_COLOR: true
|
||||
run: 'npm run test:ci -- --coverage.enabled=false'
|
||||
run: |
|
||||
if [[ "${{ matrix.shard }}" == "cli" ]]; then
|
||||
npm run test:ci --workspace @google/gemini-cli -- --coverage.enabled=false
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
- name: 'Bundle'
|
||||
run: 'npm run bundle'
|
||||
@@ -230,7 +260,7 @@ jobs:
|
||||
${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
|
||||
with:
|
||||
name: 'Test Results (Node ${{ matrix.node-version }})'
|
||||
name: 'Test Results (Node ${{ runner.os }}, ${{ matrix.node-version }}, ${{ matrix.shard }})'
|
||||
path: 'packages/*/junit.xml'
|
||||
reporter: 'java-junit'
|
||||
fail-on-error: 'false'
|
||||
@@ -240,7 +270,7 @@ jobs:
|
||||
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'test-results-fork-${{ matrix.node-version }}-${{ runner.os }}'
|
||||
name: 'test-results-fork-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
|
||||
path: 'packages/*/junit.xml'
|
||||
|
||||
- name: 'Upload coverage reports'
|
||||
@@ -248,7 +278,7 @@ jobs:
|
||||
${{ always() }}
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'
|
||||
name: 'coverage-reports-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
|
||||
path: 'packages/*/coverage'
|
||||
|
||||
codeql:
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
name: 'Evals: Nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *' # Runs at 1 AM every day
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_all:
|
||||
description: 'Run all evaluations (including usually passing)'
|
||||
type: 'boolean'
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
actions: 'read'
|
||||
|
||||
jobs:
|
||||
evals:
|
||||
name: 'Evals (USUALLY_PASSING) nightly run'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
run_attempt: [1, 2, 3]
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Create logs directory'
|
||||
run: 'mkdir -p evals/logs'
|
||||
|
||||
- name: 'Run Evals'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
RUN_EVALS: "${{ github.event.inputs.run_all != 'false' }}"
|
||||
run: 'npm run test:all_evals'
|
||||
|
||||
- name: 'Upload Logs'
|
||||
if: 'always()'
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'eval-logs-${{ matrix.run_attempt }}'
|
||||
path: 'evals/logs'
|
||||
retention-days: 7
|
||||
|
||||
aggregate-results:
|
||||
name: 'Aggregate Results'
|
||||
needs: ['evals']
|
||||
if: 'always()'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
- name: 'Download Logs'
|
||||
uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
|
||||
with:
|
||||
path: 'artifacts'
|
||||
|
||||
- name: 'Generate Summary'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: 'node scripts/aggregate_evals.js artifacts >> "$GITHUB_STEP_SUMMARY"'
|
||||
@@ -101,7 +101,6 @@ jobs:
|
||||
"FIRESTORE_DATABASE_ID": "(default)",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
|
||||
},
|
||||
"enabled": true,
|
||||
"timeout": 600000
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,9 +14,15 @@ on:
|
||||
description: 'issue number to triage'
|
||||
required: true
|
||||
type: 'number'
|
||||
workflow_call:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'issue number to triage'
|
||||
required: false
|
||||
type: 'string'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || inputs.issue_number }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
@@ -34,7 +40,7 @@ permissions:
|
||||
jobs:
|
||||
triage-issue:
|
||||
if: |-
|
||||
github.repository == 'google-gemini/gemini-cli' &&
|
||||
(github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli') &&
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
@@ -57,10 +63,11 @@ jobs:
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
script: |
|
||||
const issueNumber = ${{ github.event.inputs.issue_number || inputs.issue_number }};
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: ${{ github.event.inputs.issue_number }},
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
core.setOutput('title', issue.title);
|
||||
core.setOutput('body', issue.body);
|
||||
@@ -71,7 +78,7 @@ jobs:
|
||||
if: |-
|
||||
github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number }}'
|
||||
ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number || inputs.issue_number }}'
|
||||
LABELS: '${{ steps.get_issue_data.outputs.labels }}'
|
||||
run: |
|
||||
if echo "${LABELS}" | grep -q 'area/'; then
|
||||
@@ -86,6 +93,9 @@ jobs:
|
||||
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
env:
|
||||
APP_ID: '${{ secrets.APP_ID }}'
|
||||
if: "${{ env.APP_ID != '' }}"
|
||||
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
@@ -96,7 +106,7 @@ jobs:
|
||||
id: 'get_labels'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
script: |-
|
||||
const { data: labels } = await github.rest.issues.listLabelsForRepo({
|
||||
owner: context.repo.owner,
|
||||
@@ -127,7 +137,7 @@ jobs:
|
||||
ISSUE_BODY: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.body || github.event.issue.body }}
|
||||
ISSUE_NUMBER: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && github.event.inputs.issue_number || github.event.issue.number }}
|
||||
${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.issue_number || inputs.issue_number) || github.event.issue.number }}
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
|
||||
with:
|
||||
@@ -253,7 +263,7 @@ jobs:
|
||||
LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
script: |
|
||||
const rawOutput = process.env.LABELS_OUTPUT;
|
||||
core.info(`Raw output from model: ${rawOutput}`);
|
||||
@@ -319,7 +329,7 @@ jobs:
|
||||
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
script: |-
|
||||
github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
|
||||
@@ -81,7 +81,6 @@ jobs:
|
||||
"FIRESTORE_DATABASE_ID": "(default)",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
|
||||
},
|
||||
"enabled": true,
|
||||
"timeout": 600000
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
name: '📋 Gemini Scheduled Issue Triage'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- 'opened'
|
||||
- 'reopened'
|
||||
schedule:
|
||||
- cron: '0 * * * *' # Runs every hour
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}'
|
||||
group: '${{ github.workflow }}-${{ github.event.number || github.run_id }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
@@ -35,7 +39,19 @@ jobs:
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Get issue from event'
|
||||
if: "github.event_name == 'issues'"
|
||||
id: 'get_issue_from_event'
|
||||
env:
|
||||
ISSUE_EVENT: '${{ toJSON(github.event.issue) }}'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ISSUE_JSON=$(echo "$ISSUE_EVENT" | jq -c '[{number: .number, title: .title, body: .body}]')
|
||||
echo "issues_to_triage=${ISSUE_JSON}" >> "${GITHUB_OUTPUT}"
|
||||
echo "✅ Found issue #${{ github.event.issue.number }} from event to triage! 🎯"
|
||||
|
||||
- name: 'Find untriaged issues'
|
||||
if: "github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'"
|
||||
id: 'find_issues'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
@@ -77,12 +93,13 @@ jobs:
|
||||
|
||||
- name: 'Run Gemini Issue Analysis'
|
||||
if: |-
|
||||
${{ steps.find_issues.outputs.issues_to_triage != '[]' }}
|
||||
(steps.get_issue_from_event.outputs.issues_to_triage != '' && steps.get_issue_from_event.outputs.issues_to_triage != '[]') ||
|
||||
(steps.find_issues.outputs.issues_to_triage != '' && steps.find_issues.outputs.issues_to_triage != '[]')
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
|
||||
id: 'gemini_issue_analysis'
|
||||
env:
|
||||
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
|
||||
ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}'
|
||||
ISSUES_TO_TRIAGE: '${{ steps.get_issue_from_event.outputs.issues_to_triage || steps.find_issues.outputs.issues_to_triage }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
|
||||
with:
|
||||
@@ -116,12 +133,12 @@ jobs:
|
||||
1. You are only able to use the echo command. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
|
||||
2. Check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues)
|
||||
3. Review the issue title, body and any comments provided in the environment variables.
|
||||
4. Identify the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*.
|
||||
5. If the issue already has area/ label, dont try to change it. Similarly, if the issue already has a kind/ label don't change it. And if the issue already has a priority/ label do not change it for example:
|
||||
If an issue has area/core and kind/bug you will only add a priority/ label.
|
||||
Instead if an issue has no labels, you will could add one lable of each kind.
|
||||
4. Identify the most relevant labels from the existing labels, focusing on kind/* and priority/*.
|
||||
5. If the issue already has a kind/ label don't change it. And if the issue already has a priority/ label do not change it for example:
|
||||
If an issue has kind/bug you will only add a priority/ label.
|
||||
Instead if an issue has no labels, you could add one label of each kind.
|
||||
6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc.
|
||||
7. For area/* and kind/* limit yourself to only the single most applicable label in each case.
|
||||
7. For kind/* limit yourself to only the single most applicable label.
|
||||
8. Give me a single short explanation about why you are selecting each label in the process.
|
||||
9. Output a JSON array of objects, each containing the issue number
|
||||
and the labels to add and remove, along with an explanation. For example:
|
||||
@@ -147,7 +164,7 @@ jobs:
|
||||
11. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label and leave a comment politely requesting the relevant information, eg.. if repro steps are missing request for repro steps. if version information is missing request for version information into the explanation section below.
|
||||
- After identifying appropriate labels to an issue, add "status/need-triage" label to labels_to_remove in the output.
|
||||
12. If you think an issue might be a Priority/P0 do not apply the priority/p0 label. Instead apply a status/manual-triage label and include a note in your explanation.
|
||||
13. If you are uncertain and have not been able to apply one each of kind/, area/ and priority/ , apply the status/manual-triage label.
|
||||
13. If you are uncertain and have not been able to apply one each of kind/ and priority/ , apply the status/manual-triage label.
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -157,9 +174,8 @@ jobs:
|
||||
- Do not add comments or modify the issue content.
|
||||
- Do not remove the following labels maintainer, help wanted or good first issue.
|
||||
- Triage only the current issue.
|
||||
- Identify only one area/ label
|
||||
- Identify only one kind/ label (Do not apply kind/duplicate or kind/parent-issue)
|
||||
- Identify all applicable sub-area/* and priority/* labels based on the issue content. It's ok to have multiple of these.
|
||||
- Identify all applicable priority/* labels based on the issue content. It's ok to have multiple of these.
|
||||
- Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario.
|
||||
Categorization Guidelines:
|
||||
P0: Critical / Blocker
|
||||
@@ -206,51 +222,6 @@ jobs:
|
||||
- If users are talking about issues where the model gets downgraded from pro to flash then i want you to categorize that as a performance issue
|
||||
- This product is designed to use different models eg.. using pro, downgrading to flash etc.
|
||||
- When users report that they dont expect the model to change those would be categorized as feature requests.
|
||||
Definition of Areas
|
||||
area/ux:
|
||||
- Issues concerning user-facing elements like command usability, interactive features, help docs, and perceived performance.
|
||||
- I am seeing my screen flicker when using Gemini CLI
|
||||
- I am seeing the output malformed
|
||||
- Theme changes aren't taking effect
|
||||
- My keyboard inputs arent' being recognzied
|
||||
area/platform:
|
||||
- Issues related to installation, packaging, OS compatibility (Windows, macOS, Linux), and the underlying CLI framework.
|
||||
area/background: Issues related to long-running background tasks, daemons, and autonomous or proactive agent features.
|
||||
area/models:
|
||||
- i am not getting a response that is reasonable or expected. this can include things like
|
||||
- I am calling a tool and the tool is not performing as expected.
|
||||
- i am expecting a tool to be called and it is not getting called ,
|
||||
- Including experience when using
|
||||
- built-in tools (e.g., web search, code interpreter, read file, writefile, etc..),
|
||||
- Function calling issues should be under this area
|
||||
- i am getting responses from the model that are malformed.
|
||||
- Issues concerning Gemini quality of response and inference,
|
||||
- Issues talking about unnecessary token consumption.
|
||||
- Issues talking about Model getting stuck in a loop be watchful as this could be the root cause for issues that otherwise seem like model performance issues.
|
||||
- Memory compression
|
||||
- unexpected responses,
|
||||
- poor quality of generated code
|
||||
area/tools:
|
||||
- These are primarily issues related to Model Context Protocol
|
||||
- These are issues that mention MCP support
|
||||
- feature requests asking for support for new tools.
|
||||
area/core:
|
||||
- Issues with fundamental components like command parsing, configuration management, session state, and the main API client logic. Introducing multi-modality
|
||||
area/contribution:
|
||||
- Issues related to improving the developer contribution experience, such as CI/CD pipelines, build scripts, and test automation infrastructure.
|
||||
area/authentication:
|
||||
- Issues related to user identity, login flows, API key handling, credential storage, and access token management, unable to sign in selecting wrong authentication path etc..
|
||||
area/security-privacy:
|
||||
- Issues concerning vulnerability patching, dependency security, data sanitization, privacy controls, and preventing unauthorized data access.
|
||||
area/extensibility:
|
||||
- Issues related to the plugin system, extension APIs, or making the CLI's functionality available in other applications, github actions, ide support etc..
|
||||
area/performance:
|
||||
- Issues focused on model performance
|
||||
- Issues with running out of capacity,
|
||||
- 429 errors etc..
|
||||
- could also pertain to latency,
|
||||
- other general software performance like, memory usage, CPU consumption, and algorithmic efficiency.
|
||||
- Switching models from one to the other unexpectedly.
|
||||
|
||||
- name: 'Apply Labels to Issues'
|
||||
if: |-
|
||||
@@ -267,16 +238,23 @@ jobs:
|
||||
core.info(`Raw labels JSON: ${rawLabels}`);
|
||||
let parsedLabels;
|
||||
try {
|
||||
// First, try to parse the raw output as JSON.
|
||||
parsedLabels = JSON.parse(rawLabels.trim());
|
||||
} catch (jsonError) {
|
||||
// If that fails, check for a markdown code block.
|
||||
core.info(`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`);
|
||||
const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (!jsonMatch || !jsonMatch[1]) {
|
||||
throw new Error("Could not find a ```json ... ``` block in the output.");
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
try {
|
||||
parsedLabels = JSON.parse(jsonMatch[1].trim());
|
||||
} catch (markdownError) {
|
||||
core.setFailed(`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawLabels}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
core.setFailed(`Output is not valid JSON and does not contain a JSON markdown block.\nRaw output: ${rawLabels}`);
|
||||
return;
|
||||
}
|
||||
const jsonString = jsonMatch[1].trim();
|
||||
parsedLabels = JSON.parse(jsonString);
|
||||
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
|
||||
} catch (err) {
|
||||
core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of parsedLabels) {
|
||||
|
||||
@@ -39,3 +39,7 @@ jobs:
|
||||
GITHUB_REPOSITORY: '${{ github.repository }}'
|
||||
run: |-
|
||||
./.github/scripts/pr-triage.sh
|
||||
# If prs_needing_comment is empty, set it to [] explicitly for downstream steps
|
||||
if [[ -z "$(grep 'prs_needing_comment' "${GITHUB_OUTPUT}" | cut -d'=' -f2-)" ]]; then
|
||||
echo "prs_needing_comment=[]" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
@@ -12,6 +12,8 @@ on:
|
||||
|
||||
jobs:
|
||||
linkChecker:
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
@@ -31,6 +31,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
.gemini/*
|
||||
!.gemini/config.yaml
|
||||
!.gemini/commands/
|
||||
!.gemini/skills/
|
||||
!.gemini/settings.json
|
||||
|
||||
# Note: .gemini-clipboard/ is NOT in gitignore so Gemini can access pasted images
|
||||
|
||||
@@ -56,3 +58,5 @@ patch_output.log
|
||||
|
||||
.genkit
|
||||
.gemini-clipboard/
|
||||
.eslintcache
|
||||
evals/logs/
|
||||
|
||||
@@ -17,5 +17,6 @@ eslint.config.js
|
||||
**/generated
|
||||
gha-creds-*.json
|
||||
junit.xml
|
||||
.gemini-linters/
|
||||
Thumbs.db
|
||||
.pytest_cache
|
||||
|
||||
@@ -39,7 +39,7 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
|
||||
|
||||
```bash
|
||||
# Using npx (no installation required)
|
||||
npx https://github.com/google-gemini/gemini-cli
|
||||
npx @google/gemini-cli
|
||||
```
|
||||
|
||||
#### Install globally with npm
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -18,6 +18,28 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.22.0 - 2025-12-22
|
||||
|
||||
- 🎉**Free Tier + Gemini 3:** Free tier users now all have access to Gemini 3
|
||||
Pro & Flash. Enable in `/settings` by toggling "Preview Features" to `true`.
|
||||
- 🎉**Gemini CLI + Colab:** Gemini CLI is now pre-installed. Can be used
|
||||
headlessly in notebook cells or interactively in the built-in terminal
|
||||
([pic](https://imgur.com/a/G0Tn7vi))
|
||||
- 🎉**Gemini CLI Extensions:**
|
||||
- **Conductor:** Planning++, Gemini works with you to build out a detailed
|
||||
plan, pull in extra details as needed, ultimately to give the LLM guardrails
|
||||
with artifacts. Measure twice, implement once!
|
||||
|
||||
`gemini extensions install https://github.com/gemini-cli-extensions/conductor`
|
||||
|
||||
Blog:
|
||||
[https://developers.googleblog.com/conductor-introducing-context-driven-development-for-gemini-cli/](https://developers.googleblog.com/conductor-introducing-context-driven-development-for-gemini-cli/)
|
||||
|
||||
- **Endor Labs:** Perform code analysis, vulnerability scanning, and
|
||||
dependency checks using natural language.
|
||||
|
||||
`gemini extensions install https://github.com/endorlabs/gemini-extension`
|
||||
|
||||
## Announcements: v0.21.0 - 2025-12-15
|
||||
|
||||
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** Better, faster and cheaper than 2.5
|
||||
|
||||
+137
-209
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.21.0 - v0.21.1
|
||||
# Latest stable release: v0.22.0 - v0.22.5
|
||||
|
||||
Released: December 16, 2025
|
||||
Released: December 30, 2025
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,215 +11,143 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** If you are a paid user, you can now
|
||||
enable Gemini 3 Pro and Gemini 3 Flash. Go to `/settings` and set **Preview
|
||||
Features** to `true` to enable Gemini 3. For more information:
|
||||
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
|
||||
- **Comprehensive quota visibility:** View usage statistics for all available
|
||||
models in the `/stats` command, even those not yet used in your current
|
||||
session. ([pic](https://imgur.com/a/cKyDtYh),
|
||||
[pr](https://github.com/google-gemini/gemini-cli/pull/14764) by
|
||||
[@sehoon38](https://github.com/sehoon38))
|
||||
- **Polished CLI statistics:** We’ve cleaned up the `/stats` view to prioritize
|
||||
actionable quota information while providing a detailed token and
|
||||
cache-efficiency breakdown in `/stats model`
|
||||
([login with Google](https://imgur.com/a/w9xKthm),
|
||||
[api key](https://imgur.com/a/FjQPHOY),
|
||||
[model stats](https://imgur.com/a/VfWzVgw),
|
||||
[pr](https://github.com/google-gemini/gemini-cli/pull/14961) by
|
||||
[@jacob314](https://github.com/jacob314))
|
||||
- **Multi-file drag & drop:** Multi-file drag & drop is now supported and
|
||||
properly translated to be prefixed with `@`.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/14832) by
|
||||
[@jackwotherspoon](https://github.com/jackwotherspoon))
|
||||
|
||||
## What's Changed
|
||||
|
||||
- refactor(stdio): always patch stdout and use createWorkingStdio for clean
|
||||
output by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14159
|
||||
- chore(release): bump version to 0.21.0-nightly.20251202.2d935b379 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14409
|
||||
- implement fuzzy search inside settings by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13864
|
||||
- feat: enable message bus integration by default by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14329
|
||||
- docs: Recommend using --debug intead of --verbose for CLI debugging by @bbiggs
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14334
|
||||
- feat: consolidate remote MCP servers to use `url` in config by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/13762
|
||||
- Restrict integration tests tools by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14403
|
||||
- track github repository names in telemetry events by @IamRiddhi in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13670
|
||||
- Allow telemetry exporters to GCP to utilize user's login credentials, if
|
||||
requested by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13778
|
||||
- refactor(editor): use const assertion for editor types with single source of
|
||||
truth by @amsminn in https://github.com/google-gemini/gemini-cli/pull/8604
|
||||
- fix(security): Fix npm audit vulnerabilities in glob and body-parser by
|
||||
@afarber in https://github.com/google-gemini/gemini-cli/pull/14090
|
||||
- Add new enterprise instructions by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/8641
|
||||
- feat(hooks): Hook Session Lifecycle & Compression Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14151
|
||||
- Avoid triggering refreshStatic unless there really is a banner to display. by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14328
|
||||
- feat(hooks): Hooks Commands Panel, Enable/Disable, and Migrate by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14225
|
||||
- fix: Bundle default policies for npx distribution by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14457
|
||||
- feat(hooks): Hook System Documentation by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14307
|
||||
- Fix tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14458
|
||||
- feat: add scheduled workflow to close stale issues by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14404
|
||||
- feat: Support Extension Hooks with Security Warning by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14460
|
||||
- feat: Add enableAgents experimental flag by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14371
|
||||
- docs: fix typo 'socus' to 'focus' in todos.md by @Viktor286 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14374
|
||||
- Markdown export: move the emoji to the end of the line by @mhansen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12278
|
||||
- fix(acp): prevent unnecessary credential cache clearing on re-authent… by
|
||||
@h-michael in https://github.com/google-gemini/gemini-cli/pull/9410
|
||||
- fix(cli): Fix word navigation for CJK characters by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14475
|
||||
- Remove example extension by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14376
|
||||
- Add commands for listing and updating per-extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12664
|
||||
- chore(tests): remove obsolete test for hierarchical memory by @pareshjoshij in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13122
|
||||
- feat(cli): support /copy in remote sessions using OSC52 by @ismellpillows in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13471
|
||||
- Update setting search UX by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14451
|
||||
- Fix(cli): Improve Homebrew update instruction to specify gemini-cli by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/14502
|
||||
- do not toggle the setting item when entering space by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14489
|
||||
- fix: improve retry logic for fetch errors and network codes by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14439
|
||||
- remove unused isSearching field by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14509
|
||||
- feat(mcp): add `--type` alias for `--transport` flag in gemini mcp add by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14503
|
||||
- feat(cli): Move key restore logic to core by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13013
|
||||
- feat: add auto-execute on Enter behavior to argumentless MCP prompts by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14510
|
||||
- fix(shell): cursor visibility when using interactive mode by @aswinashok44 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14095
|
||||
- Adding session id as part of json o/p by @MJjainam in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14504
|
||||
- fix(extensions): resolve GitHub API 415 error for source tarballs by
|
||||
@jpoehnelt in https://github.com/google-gemini/gemini-cli/pull/13319
|
||||
- fix(client): Correctly latch hasFailedCompressionAttempt flag by @pareshjoshij
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13002
|
||||
- Disable flaky extension reloading test on linux by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14528
|
||||
- Add support for MCP dynamic tool update by `notifications/tools/list_changed`
|
||||
by @Adib234 in https://github.com/google-gemini/gemini-cli/pull/14375
|
||||
- Fix privacy screen for legacy tier users by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14522
|
||||
- feat: Exclude maintainer labeled issues from stale issue closer by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14532
|
||||
- Grant chained workflows proper permission. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14534
|
||||
- Make trigger_e2e manually fireable. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14547
|
||||
- Write e2e status to local repo not forked repo by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14549
|
||||
- Fixes [API Error: Cannot read properties of undefined (reading 'error')] by
|
||||
@silviojr in https://github.com/google-gemini/gemini-cli/pull/14553
|
||||
- Trigger chained e2e tests on all pull requests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14551
|
||||
- Fix bug in the shellExecutionService resulting in both truncation and 3X bloat
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14545
|
||||
- Fix issue where we were passing the model content reflecting terminal line
|
||||
wrapping. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14566
|
||||
- chore/release: bump version to 0.21.0-nightly.20251204.3da4fd5f7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14476
|
||||
- feat(sessions): use 1-line generated session summary to describe sessions by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14467
|
||||
- Use Robot PAT for chained e2e merge queue skipper by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14585
|
||||
- fix(core): improve API response error handling and retry logic by @mattKorwel
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14563
|
||||
- Docs: Model routing clarification by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14373
|
||||
- expose previewFeatures flag in a2a by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14550
|
||||
- Fix emoji width in debug console. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14593
|
||||
- Fully detach autoupgrade process by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14595
|
||||
- Docs: Update Gemini 3 on Gemini CLI documentation by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14601
|
||||
- Disallow floating promises. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14605
|
||||
- chore/release: bump version to 0.21.0-nightly.20251207.025e450ac by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14662
|
||||
- feat(modelAvailabilityService): integrate model availability service into
|
||||
backend logic by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14470
|
||||
- Add prompt_id propagation in a2a-server task by @koxkox111 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14581
|
||||
- Fix: Prevent freezing in non-interactive Gemini CLI when debug mode is enabled
|
||||
by @parthasaradhie in https://github.com/google-gemini/gemini-cli/pull/14580
|
||||
- fix(audio): improve reading of audio files by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14658
|
||||
- Update automated triage workflow to stop assigning priority labels by
|
||||
@skeshive in https://github.com/google-gemini/gemini-cli/pull/14717
|
||||
- set failed status when chained e2e fails by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14725
|
||||
- feat(github action) Triage and Label Pull Requests by Size and Comple… by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/5571
|
||||
- refactor(telemetry): Improve previous PR that allows telemetry to use the CLI
|
||||
auth and add testing by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14589
|
||||
- Always set status in chained_e2e workflow by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14730
|
||||
- feat: Add OTEL log event `gemini_cli.startup_stats` for startup stats. by
|
||||
@kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/14734
|
||||
- feat: auto-execute on slash command completion functions by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14584
|
||||
- Docs: Proper release notes by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14405
|
||||
- Add support for user-scoped extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13748
|
||||
- refactor(core): Improve environment variable handling in shell execution by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14742
|
||||
- Remove old E2E Workflows by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14749
|
||||
- fix: handle missing local extension config and skip hooks when disabled by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14744
|
||||
- chore/release: bump version to 0.21.0-nightly.20251209.ec9a8c7a7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14751
|
||||
- feat: Add support for MCP Resources by @MrLesk in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13178
|
||||
- Always set pending status in E2E tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14756
|
||||
- fix(lint): upgrade pip and use public pypi for yamllint by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14746
|
||||
- fix: use Gemini API supported image formats for clipboard by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14762
|
||||
- feat(a2a): Introduce restore command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13015
|
||||
- allow final:true to be returned on a2a server edit calls. by @DavidAPierce in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14747
|
||||
- (fix) Automated pr labeller by @DaanVersavel in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14788
|
||||
- Update CODEOWNERS by @kklashtorny1 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14830
|
||||
- Docs: Fix errors preventing site rebuild. by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14842
|
||||
- chore(deps): bump express from 5.1.0 to 5.2.0 by @dependabot[bot] in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14325
|
||||
- fix(patch): cherry-pick 3f5f030 to release/v0.21.0-preview.0-pr-14843 to patch
|
||||
version v0.21.0-preview.0 and create version 0.21.0-preview.1 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14851
|
||||
- fix(patch): cherry-pick ee6556c to release/v0.21.0-preview.1-pr-14691 to patch
|
||||
version v0.21.0-preview.1 and create version 0.21.0-preview.2 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14908
|
||||
- fix(patch): cherry-pick 54de675 to release/v0.21.0-preview.2-pr-14961 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14968
|
||||
- fix(patch): cherry-pick 12cbe32 to release/v0.21.0-preview.3-pr-15000 to patch
|
||||
version v0.21.0-preview.3 and create version 0.21.0-preview.4 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15003
|
||||
- fix(patch): cherry-pick edbe548 to release/v0.21.0-preview.4-pr-15007 to patch
|
||||
version v0.21.0-preview.4 and create version 0.21.0-preview.5 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15015
|
||||
- fix(patch): cherry-pick 2995af6 to release/v0.21.0-preview.5-pr-15131 to patch
|
||||
version v0.21.0-preview.5 and create version 0.21.0-preview.6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15153
|
||||
- feat(ide): fallback to GEMINI_CLI_IDE_AUTH_TOKEN env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14843
|
||||
- feat: display quota stats for unused models in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14764
|
||||
- feat: ensure codebase investigator uses preview model when main agent does by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14412
|
||||
- chore: add closing reason to stale bug workflow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14861
|
||||
- Send the model and CLI version with the user agent by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14865
|
||||
- refactor(sessions): move session summary generation to startup by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14691
|
||||
- Limit search depth in path corrector by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14869
|
||||
- Fix: Correct typo in code comment by @kuishou68 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14671
|
||||
- feat(core): Plumbing for late resolution of model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14597
|
||||
- feat: attempt more error parsing by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14899
|
||||
- Add missing await. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14910
|
||||
- feat(core): Add support for transcript_path in hooks for git-ai/Gemini
|
||||
extension by @svarlamov in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14663
|
||||
- refactor: implement DelegateToAgentTool with discriminated union by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14769
|
||||
- feat: reset availabilityService on /auth by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14911
|
||||
- chore/release: bump version to 0.21.0-nightly.20251211.8c83e1ea9 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14924
|
||||
- Fix: Correctly detect MCP tool errors by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14937
|
||||
- increase labeler timeout by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14922
|
||||
- tool(cli): tweak the frontend tool to be aware of more core files from the cli
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14962
|
||||
- feat(cli): polish cached token stats and simplify stats display when quota is
|
||||
present. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14961
|
||||
- feat(settings-validation): add validation for settings schema by @lifefloating
|
||||
in https://github.com/google-gemini/gemini-cli/pull/12929
|
||||
- fix(ide): Update IDE extension to write auth token in env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14999
|
||||
- Revert "chore(deps): bump express from 5.1.0 to 5.2.0" by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14998
|
||||
- feat(a2a): Introduce /init command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13419
|
||||
- feat: support multi-file drag and drop of images by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14832
|
||||
- fix(policy): allow codebase_investigator by default in read-only policy by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15000
|
||||
- refactor(ide ext): Update port file name + switch to 1-based index for
|
||||
characters + remove truncation text by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/10501
|
||||
- fix(vscode-ide-companion): correct license generation for workspace
|
||||
dependencies by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15004
|
||||
- fix: temp fix for subagent invocation until subagent delegation is merged to
|
||||
stable by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15007
|
||||
- test: update ide detection tests to make them more robust when run in an ide
|
||||
by @kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/15008
|
||||
- Remove flex from stats display. See snapshots for diffs. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14983
|
||||
- Add license field into package.json by @jb-perez in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14473
|
||||
- feat: Persistent "Always Allow" policies with granular shell & MCP support by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/14737
|
||||
- chore/release: bump version to 0.21.0-nightly.20251212.54de67536 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14969
|
||||
- fix(core): commandPrefix word boundary and compound command safety by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/15006
|
||||
- chore(docs): add 'Maintainers only' label info to CONTRIBUTING.md by @jacob314
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14914
|
||||
- Refresh hooks when refreshing extensions. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14918
|
||||
- Add clarity to error messages by @gsehgal in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14879
|
||||
- chore : remove a redundant tip by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14947
|
||||
- chore/release: bump version to 0.21.0-nightly.20251213.977248e09 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15029
|
||||
- Disallow redundant typecasts. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15030
|
||||
- fix(auth): prioritize GEMINI_API_KEY env var and skip unnecessary key… by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14745
|
||||
- fix: use zod for safety check result validation by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15026
|
||||
- update(telemetry): add hashed_extension_name to field to extension events by
|
||||
@kiranani in https://github.com/google-gemini/gemini-cli/pull/15025
|
||||
- fix: similar to policy-engine, throw error in case of requiring tool execution
|
||||
confirmation for non-interactive mode by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14702
|
||||
- Clean up processes in integration tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15102
|
||||
- docs: update policy engine getting started and defaults by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15105
|
||||
- Fix tool output fragmentation by encapsulating content in functionResponse by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/13082
|
||||
- Simplify method signature. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15114
|
||||
- Show raw input token counts in json output. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15021
|
||||
- fix: Mark A2A requests as interactive by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15108
|
||||
- use previewFeatures to determine which pro model to use for A2A by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15131
|
||||
- refactor(cli): fix settings merging so that settings using the new json format
|
||||
take priority over ones using the old format by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15116
|
||||
- fix(patch): cherry-pick a6d1245 to release/v0.22.0-preview.1-pr-15214 to patch
|
||||
version v0.22.0-preview.1 and create version 0.22.0-preview.2 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15226
|
||||
- fix(patch): cherry-pick 9e6914d to release/v0.22.0-preview.2-pr-15288 to patch
|
||||
version v0.22.0-preview.2 and create version 0.22.0-preview.3 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15294
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.20.2...v0.21.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.21.3...v0.22.0
|
||||
|
||||
+116
-114
@@ -1,6 +1,6 @@
|
||||
# Preview release: Release v0.22.0-preview.0
|
||||
# Preview release: Release v0.23.0-preview.0
|
||||
|
||||
Released: December 16, 2025
|
||||
Released: December 22, 2025
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -13,117 +13,119 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## What's Changed
|
||||
|
||||
- feat(ide): fallback to GEMINI_CLI_IDE_AUTH_TOKEN env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14843
|
||||
- feat: display quota stats for unused models in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14764
|
||||
- feat: ensure codebase investigator uses preview model when main agent does by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14412
|
||||
- chore: add closing reason to stale bug workflow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14861
|
||||
- Send the model and CLI version with the user agent by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14865
|
||||
- refactor(sessions): move session summary generation to startup by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14691
|
||||
- Limit search depth in path corrector by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14869
|
||||
- Fix: Correct typo in code comment by @kuishou68 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14671
|
||||
- feat(core): Plumbing for late resolution of model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14597
|
||||
- feat: attempt more error parsing by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14899
|
||||
- Add missing await. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14910
|
||||
- feat(core): Add support for transcript_path in hooks for git-ai/Gemini
|
||||
extension by @svarlamov in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14663
|
||||
- refactor: implement DelegateToAgentTool with discriminated union by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14769
|
||||
- feat: reset availabilityService on /auth by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14911
|
||||
- chore/release: bump version to 0.21.0-nightly.20251211.8c83e1ea9 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14924
|
||||
- Fix: Correctly detect MCP tool errors by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14937
|
||||
- increase labeler timeout by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14922
|
||||
- tool(cli): tweak the frontend tool to be aware of more core files from the cli
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14962
|
||||
- feat(cli): polish cached token stats and simplify stats display when quota is
|
||||
present. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14961
|
||||
- feat(settings-validation): add validation for settings schema by @lifefloating
|
||||
in https://github.com/google-gemini/gemini-cli/pull/12929
|
||||
- fix(ide): Update IDE extension to write auth token in env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14999
|
||||
- Revert "chore(deps): bump express from 5.1.0 to 5.2.0" by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14998
|
||||
- feat(a2a): Introduce /init command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13419
|
||||
- feat: support multi-file drag and drop of images by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14832
|
||||
- fix(policy): allow codebase_investigator by default in read-only policy by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15000
|
||||
- refactor(ide ext): Update port file name + switch to 1-based index for
|
||||
characters + remove truncation text by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/10501
|
||||
- fix(vscode-ide-companion): correct license generation for workspace
|
||||
dependencies by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15004
|
||||
- fix: temp fix for subagent invocation until subagent delegation is merged to
|
||||
stable by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15007
|
||||
- test: update ide detection tests to make them more robust when run in an ide
|
||||
by @kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/15008
|
||||
- Remove flex from stats display. See snapshots for diffs. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14983
|
||||
- Add license field into package.json by @jb-perez in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14473
|
||||
- feat: Persistent "Always Allow" policies with granular shell & MCP support by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/14737
|
||||
- chore/release: bump version to 0.21.0-nightly.20251212.54de67536 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14969
|
||||
- fix(core): commandPrefix word boundary and compound command safety by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/15006
|
||||
- chore(docs): add 'Maintainers only' label info to CONTRIBUTING.md by @jacob314
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14914
|
||||
- Refresh hooks when refreshing extensions. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14918
|
||||
- Add clarity to error messages by @gsehgal in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14879
|
||||
- chore : remove a redundant tip by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14947
|
||||
- chore/release: bump version to 0.21.0-nightly.20251213.977248e09 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15029
|
||||
- Disallow redundant typecasts. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15030
|
||||
- fix(auth): prioritize GEMINI_API_KEY env var and skip unnecessary key… by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14745
|
||||
- fix: use zod for safety check result validation by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15026
|
||||
- update(telemetry): add hashed_extension_name to field to extension events by
|
||||
@kiranani in https://github.com/google-gemini/gemini-cli/pull/15025
|
||||
- fix: similar to policy-engine, throw error in case of requiring tool execution
|
||||
confirmation for non-interactive mode by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14702
|
||||
- Clean up processes in integration tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15102
|
||||
- docs: update policy engine getting started and defaults by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15105
|
||||
- Fix tool output fragmentation by encapsulating content in functionResponse by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/13082
|
||||
- Simplify method signature. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15114
|
||||
- Show raw input token counts in json output. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15021
|
||||
- fix: Mark A2A requests as interactive by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15108
|
||||
- use previewFeatures to determine which pro model to use for A2A by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15131
|
||||
- refactor(cli): fix settings merging so that settings using the new json format
|
||||
take priority over ones using the old format by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15116
|
||||
- Code assist service metrics. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15024
|
||||
- chore/release: bump version to 0.21.0-nightly.20251216.bb0c0d8ee by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15121
|
||||
- Docs by @Roaimkhan in https://github.com/google-gemini/gemini-cli/pull/15103
|
||||
- Use official ACP SDK and support HTTP/SSE based MCP servers by @SteffenDE in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13856
|
||||
- Remove foreground for themes other than shades of purple and holiday. by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14606
|
||||
- chore: remove repo specific tips by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15164
|
||||
- chore: remove user query from footer in debug mode by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15169
|
||||
- Disallow unnecessary awaits. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15172
|
||||
- Add one to the padding in settings dialog to avoid flicker. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15173
|
||||
- feat(core): introduce remote agent infrastructure and rename local executor by
|
||||
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/15110
|
||||
- feat(cli): Add `/auth logout` command to clear credentials and auth state by
|
||||
@CN-Scars in https://github.com/google-gemini/gemini-cli/pull/13383
|
||||
- (fix) Automated pr labeler by @DaanVersavel in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14885
|
||||
- feat: launch Gemini 3 Flash in Gemini CLI ⚡️⚡️⚡️ by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15196
|
||||
- Refactor: Migrate console.error in ripGrep.ts to debugLogger by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15201
|
||||
- chore: update a2a-js to 0.3.7 by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15197
|
||||
- chore(core): remove redundant isModelAvailabilityServiceEnabled toggle and
|
||||
clean up dead code by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15207
|
||||
- feat(core): Late resolve `GenerateContentConfig`s and reduce mutation. by
|
||||
@joshualitt in https://github.com/google-gemini/gemini-cli/pull/14920
|
||||
- Respect previewFeatures value from the remote flag if undefined by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15214
|
||||
- feat(ui): add Windows clipboard image support and Alt+V paste workaround by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/15218
|
||||
- chore(core): remove legacy fallback flags and migrate loop detection by
|
||||
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/15213
|
||||
- fix(ui): Prevent eager slash command completion hiding sibling commands by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15224
|
||||
- Docs: Update Changelog for Dec 17, 2025 by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15204
|
||||
- Code Assist backend telemetry for user accept/reject of suggestions by
|
||||
@gundermanc in https://github.com/google-gemini/gemini-cli/pull/15206
|
||||
- fix(cli): correct initial history length handling for chat commands by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15223
|
||||
- chore/release: bump version to 0.21.0-nightly.20251218.739c02bd6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15231
|
||||
- Change detailed model stats to use a new shared Table class to resolve
|
||||
robustness issues. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15208
|
||||
- feat: add agent toml parser by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15112
|
||||
- Add core tool that adds all context from the core package. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15238
|
||||
- (docs): Add reference section to hooks documentation by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15159
|
||||
- feat(hooks): add support for friendly names and descriptions by @abhipatel12
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15174
|
||||
- feat: Detect background color by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15132
|
||||
- add 3.0 to allowed sensitive keywords by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15276
|
||||
- feat: Pass additional environment variables to shell execution by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15160
|
||||
- Remove unused code by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15290
|
||||
- Handle all 429 as retryableQuotaError by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15288
|
||||
- Remove unnecessary dependencies by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15291
|
||||
- fix: prevent infinite loop in prompt completion on error by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14548
|
||||
- fix(ui): show command suggestions even on perfect match and sort them by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15287
|
||||
- feat(hooks): reduce log verbosity and improve error reporting in UI by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15297
|
||||
- feat: simplify tool confirmation labels for better UX by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15296
|
||||
- chore/release: bump version to 0.21.0-nightly.20251219.70696e364 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15301
|
||||
- feat(core): Implement JIT context memory loading and UI sync by @SandyTao520
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14469
|
||||
- feat(ui): Put "Allow for all future sessions" behind a setting off by default.
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/15322
|
||||
- fix(cli):change the placeholder of input during the shell mode by
|
||||
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/15135
|
||||
- Validate OAuth resource parameter matches MCP server URL by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15289
|
||||
- docs(cli): add System Prompt Override (GEMINI_SYSTEM_MD) by @ashmod in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9515
|
||||
- more robust command parsing logs by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15339
|
||||
- Introspection agent demo by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15232
|
||||
- fix(core): sanitize hook command expansion and prevent injection by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15343
|
||||
- fix(folder trust): add validation for trusted folder level by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12215
|
||||
- fix(cli): fix right border overflow in trust dialogs by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15350
|
||||
- fix(policy): fix bug where accepting-edits continued after it was turned off
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/15351
|
||||
- fix: prevent infinite relaunch loop when --resume fails (#14941) by @Ying-xi
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14951
|
||||
- chore/release: bump version to 0.21.0-nightly.20251220.41a1a3eed by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15352
|
||||
- feat(telemetry): add clearcut logging for hooks by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15405
|
||||
- fix(core): Add `.geminiignore` support to SearchText tool by @xyrolle in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13763
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.21.0-preview.6...v0.22.0-preview.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.22.0-preview.3...v0.23.0-preview.0
|
||||
|
||||
+273
-7
@@ -12,13 +12,279 @@ on GitHub.
|
||||
|
||||
## Current Releases
|
||||
|
||||
| Release channel | Notes |
|
||||
| :------------------------------------------ | :---------------------------------------------- |
|
||||
| Nightly | Nightly release with the most recent changes. |
|
||||
| [Preview](#release-v0220-preview-0-preview) | Experimental features ready for early feedback. |
|
||||
| [Latest](#release-v0210---v0211-latest) | Stable, recommended for general use. |
|
||||
| Release channel | Notes |
|
||||
| :---------------------------------------- | :---------------------------------------------- |
|
||||
| Nightly | Nightly release with the most recent changes. |
|
||||
| [Preview](#release-v0230-preview-preview) | Experimental features ready for early feedback. |
|
||||
| [Latest](#release-v0220---v0225-latest) | Stable, recommended for general use. |
|
||||
|
||||
## Release v0.21.0 - v0.21.1 (Latest)
|
||||
## Release v0.23.0-preview (Preview)
|
||||
|
||||
## What's Changed
|
||||
|
||||
- Code assist service metrics. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15024
|
||||
- chore/release: bump version to 0.21.0-nightly.20251216.bb0c0d8ee by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15121
|
||||
- Docs by @Roaimkhan in https://github.com/google-gemini/gemini-cli/pull/15103
|
||||
- Use official ACP SDK and support HTTP/SSE based MCP servers by @SteffenDE in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13856
|
||||
- Remove foreground for themes other than shades of purple and holiday. by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14606
|
||||
- chore: remove repo specific tips by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15164
|
||||
- chore: remove user query from footer in debug mode by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15169
|
||||
- Disallow unnecessary awaits. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15172
|
||||
- Add one to the padding in settings dialog to avoid flicker. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15173
|
||||
- feat(core): introduce remote agent infrastructure and rename local executor by
|
||||
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/15110
|
||||
- feat(cli): Add `/auth logout` command to clear credentials and auth state by
|
||||
@CN-Scars in https://github.com/google-gemini/gemini-cli/pull/13383
|
||||
- (fix) Automated pr labeler by @DaanVersavel in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14885
|
||||
- feat: launch Gemini 3 Flash in Gemini CLI ⚡️⚡️⚡️ by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15196
|
||||
- Refactor: Migrate console.error in ripGrep.ts to debugLogger by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15201
|
||||
- chore: update a2a-js to 0.3.7 by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15197
|
||||
- chore(core): remove redundant isModelAvailabilityServiceEnabled toggle and
|
||||
clean up dead code by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15207
|
||||
- feat(core): Late resolve `GenerateContentConfig`s and reduce mutation. by
|
||||
@joshualitt in https://github.com/google-gemini/gemini-cli/pull/14920
|
||||
- Respect previewFeatures value from the remote flag if undefined by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15214
|
||||
- feat(ui): add Windows clipboard image support and Alt+V paste workaround by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/15218
|
||||
- chore(core): remove legacy fallback flags and migrate loop detection by
|
||||
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/15213
|
||||
- fix(ui): Prevent eager slash command completion hiding sibling commands by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15224
|
||||
- Docs: Update Changelog for Dec 17, 2025 by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15204
|
||||
- Code Assist backend telemetry for user accept/reject of suggestions by
|
||||
@gundermanc in https://github.com/google-gemini/gemini-cli/pull/15206
|
||||
- fix(cli): correct initial history length handling for chat commands by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15223
|
||||
- chore/release: bump version to 0.21.0-nightly.20251218.739c02bd6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15231
|
||||
- Change detailed model stats to use a new shared Table class to resolve
|
||||
robustness issues. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15208
|
||||
- feat: add agent toml parser by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15112
|
||||
- Add core tool that adds all context from the core package. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15238
|
||||
- (docs): Add reference section to hooks documentation by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15159
|
||||
- feat(hooks): add support for friendly names and descriptions by @abhipatel12
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15174
|
||||
- feat: Detect background color by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15132
|
||||
- add 3.0 to allowed sensitive keywords by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15276
|
||||
- feat: Pass additional environment variables to shell execution by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15160
|
||||
- Remove unused code by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15290
|
||||
- Handle all 429 as retryableQuotaError by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15288
|
||||
- Remove unnecessary dependencies by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15291
|
||||
- fix: prevent infinite loop in prompt completion on error by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14548
|
||||
- fix(ui): show command suggestions even on perfect match and sort them by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15287
|
||||
- feat(hooks): reduce log verbosity and improve error reporting in UI by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15297
|
||||
- feat: simplify tool confirmation labels for better UX by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15296
|
||||
- chore/release: bump version to 0.21.0-nightly.20251219.70696e364 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15301
|
||||
- feat(core): Implement JIT context memory loading and UI sync by @SandyTao520
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14469
|
||||
- feat(ui): Put "Allow for all future sessions" behind a setting off by default.
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/15322
|
||||
- fix(cli):change the placeholder of input during the shell mode by
|
||||
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/15135
|
||||
- Validate OAuth resource parameter matches MCP server URL by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15289
|
||||
- docs(cli): add System Prompt Override (GEMINI_SYSTEM_MD) by @ashmod in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9515
|
||||
- more robust command parsing logs by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15339
|
||||
- Introspection agent demo by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15232
|
||||
- fix(core): sanitize hook command expansion and prevent injection by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/15343
|
||||
- fix(folder trust): add validation for trusted folder level by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12215
|
||||
- fix(cli): fix right border overflow in trust dialogs by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15350
|
||||
- fix(policy): fix bug where accepting-edits continued after it was turned off
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/15351
|
||||
- fix: prevent infinite relaunch loop when --resume fails (#14941) by @Ying-xi
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14951
|
||||
- chore/release: bump version to 0.21.0-nightly.20251220.41a1a3eed by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15352
|
||||
- feat(telemetry): add clearcut logging for hooks by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15405
|
||||
- fix(core): Add `.geminiignore` support to SearchText tool by @xyrolle in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13763
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.22.0-preview.3...v0.23.0-preview.0
|
||||
|
||||
## Release v0.22.0 - v0.22.5 (Latest)
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Comprehensive quota visibility:** View usage statistics for all available
|
||||
models in the `/stats` command, even those not yet used in your current
|
||||
session. ([pic](https://imgur.com/a/cKyDtYh),
|
||||
[pr](https://github.com/google-gemini/gemini-cli/pull/14764) by
|
||||
[@sehoon38](https://github.com/sehoon38))
|
||||
- **Polished CLI statistics:** We’ve cleaned up the `/stats` view to prioritize
|
||||
actionable quota information while providing a detailed token and
|
||||
cache-efficiency breakdown in `/stats model`
|
||||
([login with Google](https://imgur.com/a/w9xKthm),
|
||||
[api key](https://imgur.com/a/FjQPHOY),
|
||||
[model stats](https://imgur.com/a/VfWzVgw),
|
||||
[pr](https://github.com/google-gemini/gemini-cli/pull/14961) by
|
||||
[@jacob314](https://github.com/jacob314))
|
||||
- **Multi-file drag & drop:** Multi-file drag & drop is now supported and
|
||||
properly translated to be prefixed with `@`.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/14832) by
|
||||
[@jackwotherspoon](https://github.com/jackwotherspoon))
|
||||
|
||||
### What's Changed
|
||||
|
||||
- feat(ide): fallback to GEMINI_CLI_IDE_AUTH_TOKEN env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14843
|
||||
- feat: display quota stats for unused models in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14764
|
||||
- feat: ensure codebase investigator uses preview model when main agent does by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14412
|
||||
- chore: add closing reason to stale bug workflow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14861
|
||||
- Send the model and CLI version with the user agent by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14865
|
||||
- refactor(sessions): move session summary generation to startup by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14691
|
||||
- Limit search depth in path corrector by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14869
|
||||
- Fix: Correct typo in code comment by @kuishou68 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14671
|
||||
- feat(core): Plumbing for late resolution of model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14597
|
||||
- feat: attempt more error parsing by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14899
|
||||
- Add missing await. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14910
|
||||
- feat(core): Add support for transcript_path in hooks for git-ai/Gemini
|
||||
extension by @svarlamov in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14663
|
||||
- refactor: implement DelegateToAgentTool with discriminated union by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14769
|
||||
- feat: reset availabilityService on /auth by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14911
|
||||
- chore/release: bump version to 0.21.0-nightly.20251211.8c83e1ea9 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14924
|
||||
- Fix: Correctly detect MCP tool errors by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14937
|
||||
- increase labeler timeout by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14922
|
||||
- tool(cli): tweak the frontend tool to be aware of more core files from the cli
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14962
|
||||
- feat(cli): polish cached token stats and simplify stats display when quota is
|
||||
present. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14961
|
||||
- feat(settings-validation): add validation for settings schema by @lifefloating
|
||||
in https://github.com/google-gemini/gemini-cli/pull/12929
|
||||
- fix(ide): Update IDE extension to write auth token in env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14999
|
||||
- Revert "chore(deps): bump express from 5.1.0 to 5.2.0" by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14998
|
||||
- feat(a2a): Introduce /init command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13419
|
||||
- feat: support multi-file drag and drop of images by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14832
|
||||
- fix(policy): allow codebase_investigator by default in read-only policy by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15000
|
||||
- refactor(ide ext): Update port file name + switch to 1-based index for
|
||||
characters + remove truncation text by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/10501
|
||||
- fix(vscode-ide-companion): correct license generation for workspace
|
||||
dependencies by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15004
|
||||
- fix: temp fix for subagent invocation until subagent delegation is merged to
|
||||
stable by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15007
|
||||
- test: update ide detection tests to make them more robust when run in an ide
|
||||
by @kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/15008
|
||||
- Remove flex from stats display. See snapshots for diffs. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14983
|
||||
- Add license field into package.json by @jb-perez in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14473
|
||||
- feat: Persistent "Always Allow" policies with granular shell & MCP support by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/14737
|
||||
- chore/release: bump version to 0.21.0-nightly.20251212.54de67536 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14969
|
||||
- fix(core): commandPrefix word boundary and compound command safety by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/15006
|
||||
- chore(docs): add 'Maintainers only' label info to CONTRIBUTING.md by @jacob314
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14914
|
||||
- Refresh hooks when refreshing extensions. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14918
|
||||
- Add clarity to error messages by @gsehgal in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14879
|
||||
- chore : remove a redundant tip by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14947
|
||||
- chore/release: bump version to 0.21.0-nightly.20251213.977248e09 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15029
|
||||
- Disallow redundant typecasts. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15030
|
||||
- fix(auth): prioritize GEMINI_API_KEY env var and skip unnecessary key… by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14745
|
||||
- fix: use zod for safety check result validation by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15026
|
||||
- update(telemetry): add hashed_extension_name to field to extension events by
|
||||
@kiranani in https://github.com/google-gemini/gemini-cli/pull/15025
|
||||
- fix: similar to policy-engine, throw error in case of requiring tool execution
|
||||
confirmation for non-interactive mode by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14702
|
||||
- Clean up processes in integration tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15102
|
||||
- docs: update policy engine getting started and defaults by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15105
|
||||
- Fix tool output fragmentation by encapsulating content in functionResponse by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/13082
|
||||
- Simplify method signature. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15114
|
||||
- Show raw input token counts in json output. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15021
|
||||
- fix: Mark A2A requests as interactive by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15108
|
||||
- use previewFeatures to determine which pro model to use for A2A by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15131
|
||||
- refactor(cli): fix settings merging so that settings using the new json format
|
||||
take priority over ones using the old format by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15116
|
||||
- fix(patch): cherry-pick a6d1245 to release/v0.22.0-preview.1-pr-15214 to patch
|
||||
version v0.22.0-preview.1 and create version 0.22.0-preview.2 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15226
|
||||
- fix(patch): cherry-pick 9e6914d to release/v0.22.0-preview.2-pr-15288 to patch
|
||||
version v0.22.0-preview.2 and create version 0.22.0-preview.3 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15294
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.21.3...v0.22.0
|
||||
|
||||
## Release v0.21.0 - v0.21.1
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -206,7 +472,7 @@ on GitHub.
|
||||
https://github.com/google-gemini/gemini-cli/pull/13015
|
||||
- allow final:true to be returned on a2a server edit calls. by @DavidAPierce in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14747
|
||||
- (fix) Automated pr labeller by @DaanVersavel in
|
||||
- (fix) Automated pr labeler by @DaanVersavel in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14788
|
||||
- Update CODEOWNERS by @kklashtorny1 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14830
|
||||
|
||||
@@ -73,6 +73,9 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **`/copy`**
|
||||
- **Description:** Copies the last output produced by Gemini CLI to your
|
||||
clipboard, for easy sharing or reuse.
|
||||
- **Behavior:**
|
||||
- Local sessions use system clipboard tools (pbcopy/xclip/clip).
|
||||
- Remote sessions (SSH/WSL) use OSC 52 and require terminal support.
|
||||
- **Note:** This command requires platform-specific clipboard tools to be
|
||||
installed.
|
||||
- On Linux, it requires `xclip` or `xsel`. You can typically install them
|
||||
|
||||
@@ -1,780 +0,0 @@
|
||||
# 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:** `~/.gemini/settings.json` (where `~` is your home directory).
|
||||
- **Scope:** Applies to all Gemini CLI sessions for the current user.
|
||||
- **Project settings file:**
|
||||
- **Location:** `.gemini/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). The path can
|
||||
be overridden using the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` environment
|
||||
variable.
|
||||
- **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.,
|
||||
`.gemini/sandbox-macos-custom.sb`, `.gemini/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.
|
||||
|
||||
- **`allowMCPServers`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of MCP server names that
|
||||
should be made available to the model. This can be used to restrict the set
|
||||
of MCP servers to connect to. Note that this will be ignored if
|
||||
`--allowed-mcp-server-names` is set.
|
||||
- **Default:** All MCP servers are available for use by the Gemini model.
|
||||
- **Example:** `"allowMCPServers": ["myPythonServer"]`.
|
||||
- **Security Note:** This uses simple string matching on MCP server names,
|
||||
which can be modified. If you're a system administrator looking to prevent
|
||||
users from bypassing this, consider configuring the `mcpServers` at the
|
||||
system settings level such that the user will not be able to configure any
|
||||
MCP servers of their own. This should not be used as an airtight security
|
||||
mechanism.
|
||||
|
||||
- **`excludeMCPServers`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of MCP server names that
|
||||
should be excluded from the model. A server listed in both
|
||||
`excludeMCPServers` and `allowMCPServers` is excluded. Note that this will
|
||||
be ignored if `--allowed-mcp-server-names` is set.
|
||||
- **Default**: No MCP servers excluded.
|
||||
- **Example:** `"excludeMCPServers": ["myNodeServer"]`.
|
||||
- **Security note:** This uses simple string matching on MCP server names,
|
||||
which can be modified. If you're a system administrator looking to prevent
|
||||
users from bypassing this, consider configuring the `mcpServers` at the
|
||||
system settings level such that the user will not be able to configure any
|
||||
MCP servers of their own. This should not be used as an airtight security
|
||||
mechanism.
|
||||
|
||||
- **`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"`
|
||||
|
||||
- **`vimMode`** (boolean):
|
||||
- **Description:** Enables or disables vim mode for input editing. When
|
||||
enabled, the input area supports vim-style navigation and editing commands
|
||||
with NORMAL and INSERT modes. The vim mode status is displayed in the footer
|
||||
and persists between sessions.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"vimMode": true`
|
||||
|
||||
- **`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:**
|
||||
- **`<SERVER_NAME>`** (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.
|
||||
- `includeTools` (array of strings, optional): List of tool names to
|
||||
include from this MCP server. When specified, only the tools listed here
|
||||
will be available from this server (whitelist behavior). If not
|
||||
specified, all tools from the server are enabled by default.
|
||||
- `excludeTools` (array of strings, optional): List of tool names to
|
||||
exclude from this MCP server. Tools listed here will not be available to
|
||||
the model, even if they are exposed by the server. **Note:**
|
||||
`excludeTools` takes precedence over `includeTools` - if a tool is in
|
||||
both lists, it will be excluded.
|
||||
- **Example:**
|
||||
```json
|
||||
"mcpServers": {
|
||||
"myPythonServer": {
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--port", "8080"],
|
||||
"cwd": "./mcp_tools/python",
|
||||
"timeout": 5000,
|
||||
"includeTools": ["safe_tool", "file_reader"],
|
||||
},
|
||||
"myNodeServer": {
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js"],
|
||||
"cwd": "./mcp_tools/node",
|
||||
"excludeTools": ["dangerous_tool", "file_deleter"]
|
||||
},
|
||||
"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
|
||||
```
|
||||
|
||||
- **`summarizeToolOutput`** (object):
|
||||
- **Description:** Enables or disables the summarization of tool output. You
|
||||
can specify the token budget for the summarization using the `tokenBudget`
|
||||
setting.
|
||||
- Note: Currently only the `run_shell_command` tool is supported.
|
||||
- **Default:** `{}` (Disabled by default)
|
||||
- **Example:**
|
||||
```json
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 2000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`excludedProjectEnvVars`** (array of strings):
|
||||
- **Description:** Specifies environment variables that should be excluded
|
||||
from being loaded from project `.env` files. This prevents project-specific
|
||||
environment variables (like `DEBUG=true`) from interfering with gemini-cli
|
||||
behavior. Variables from `.gemini/.env` files are never excluded.
|
||||
- **Default:** `["DEBUG", "DEBUG_MODE"]`
|
||||
- **Example:**
|
||||
```json
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
|
||||
```
|
||||
|
||||
- **`includeDirectories`** (array of strings):
|
||||
- **Description:** Specifies an array of additional absolute or relative paths
|
||||
to include in the workspace context. This allows you to work with files
|
||||
across multiple directories as if they were one. Paths can use `~` to refer
|
||||
to the user's home directory. This setting can be combined with the
|
||||
`--include-directories` command-line flag.
|
||||
- **Default:** `[]`
|
||||
- **Example:**
|
||||
```json
|
||||
"includeDirectories": [
|
||||
"/path/to/another/project",
|
||||
"../shared-library",
|
||||
"~/common-utils"
|
||||
]
|
||||
```
|
||||
|
||||
- **`loadMemoryFromIncludeDirectories`** (boolean):
|
||||
- **Description:** Controls the behavior of the `/memory refresh` command. If
|
||||
set to `true`, `GEMINI.md` files should be loaded from all directories that
|
||||
are added. If set to `false`, `GEMINI.md` should only be loaded from the
|
||||
current directory.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
```json
|
||||
"loadMemoryFromIncludeDirectories": 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,
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 100
|
||||
}
|
||||
},
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"],
|
||||
"includeDirectories": ["path/to/dir1", "~/path/to/dir2", "../path/to/dir3"],
|
||||
"loadMemoryFromIncludeDirectories": 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:** `~/.gemini/tmp/<project_hash>/shell_history`
|
||||
- `<project_hash>` is a unique identifier generated from your project's root
|
||||
path.
|
||||
- The history is stored in a file named `shell_history`.
|
||||
|
||||
## Environment variables and `.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).
|
||||
|
||||
**Environment variable exclusion:** Some environment variables (like `DEBUG` and
|
||||
`DEBUG_MODE`) are automatically excluded from being loaded from project `.env`
|
||||
files to prevent interference with gemini-cli behavior. Variables from
|
||||
`.gemini/.env` files are never excluded. You can customize this behavior using
|
||||
the `excludedProjectEnvVars` setting in your `settings.json` file.
|
||||
|
||||
- **`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"`
|
||||
- **`GEMINI_CLI_CUSTOM_HEADERS`**:
|
||||
- Adds extra HTTP headers to Gemini API and Code Assist requests.
|
||||
- Accepts a comma-separated list of `Name: value` pairs.
|
||||
- Example:
|
||||
`export GEMINI_CLI_CUSTOM_HEADERS="X-My-Header: foo, X-Trace-ID: abc123"`.
|
||||
- **`GEMINI_API_KEY_AUTH_MECHANISM`**:
|
||||
- Specifies how the API key should be sent for authentication when using
|
||||
`AuthType.USE_GEMINI` or `AuthType.USE_VERTEX_AI`.
|
||||
- Valid values are `x-goog-api-key` (default) or `bearer`.
|
||||
- If set to `bearer`, the API key will be sent in the
|
||||
`Authorization: Bearer <key>` header.
|
||||
- Example: `export GEMINI_API_KEY_AUTH_MECHANISM="bearer"`
|
||||
- **`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.
|
||||
- **`HTTP_PROXY` / `HTTPS_PROXY`**:
|
||||
- Specifies the proxy server to use for outgoing HTTP/HTTPS requests.
|
||||
- Example: `export HTTPS_PROXY="http://proxy.example.com:8080"`
|
||||
- **`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.
|
||||
- `<profile_name>`: Uses a custom profile. To define a custom profile, create
|
||||
a file named `sandbox-macos-<profile_name>.sb` in your project's `.gemini/`
|
||||
directory (e.g., `my-project/.gemini/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.
|
||||
- **Note:** These variables are automatically excluded from project `.env`
|
||||
files by default to prevent interference with gemini-cli behavior. Use
|
||||
`.gemini/.env` files if you need to set these for gemini-cli specifically.
|
||||
- **`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.
|
||||
- **`GEMINI_SYSTEM_MD`**:
|
||||
- Overrides the base system prompt with the contents of a Markdown file.
|
||||
- If set to `1` or `true`, it uses the file at `.gemini/system.md`.
|
||||
- If set to a file path, it uses that file. The path can be absolute or
|
||||
relative. `~` is supported for the home directory.
|
||||
- The specified file must exist.
|
||||
- **`GEMINI_WRITE_SYSTEM_MD`**:
|
||||
- Writes the default system prompt to a file. This is useful for getting a
|
||||
template to customize.
|
||||
- If set to `1` or `true`, it writes to `.gemini/system.md`.
|
||||
- If set to a file path, it writes to that path. The path can be absolute or
|
||||
relative. `~` is supported for the home directory. **Note: This will
|
||||
overwrite the file if it already exists.**
|
||||
|
||||
## Command-line arguments
|
||||
|
||||
Arguments passed directly when running the CLI can override other configurations
|
||||
for that specific session.
|
||||
|
||||
- **`--model <model_name>`** (**`-m <model_name>`**):
|
||||
- Specifies the Gemini model to use for this session.
|
||||
- Example: `npm start -- --model gemini-1.5-pro-latest`
|
||||
- **`--prompt <your_prompt>`** (**`-p <your_prompt>`**):
|
||||
- Used to pass a prompt directly to the command. This invokes Gemini CLI in a
|
||||
non-interactive mode.
|
||||
- **`--prompt-interactive <your_prompt>`** (**`-i <your_prompt>`**):
|
||||
- Starts an interactive session with the provided prompt as the initial input.
|
||||
- The prompt is processed within the interactive session, not before it.
|
||||
- Cannot be used when piping input from stdin.
|
||||
- Example: `gemini -i "explain this code"`
|
||||
- **`--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.
|
||||
- **`--extensions <extension_name ...>`** (**`-e <extension_name ...>`**):
|
||||
- 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.
|
||||
- **`--include-directories <dir1,dir2,...>`**:
|
||||
- Includes additional directories in the workspace for multi-directory
|
||||
support.
|
||||
- Can be specified multiple times or as comma-separated values.
|
||||
- 5 directories can be added at maximum.
|
||||
- Example: `--include-directories /path/to/project1,/path/to/project2` or
|
||||
`--include-directories /path/to/project1 --include-directories /path/to/project2`
|
||||
- **`--version`**:
|
||||
- Displays the version of the CLI.
|
||||
|
||||
## 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: `~/.gemini/<contextFileName>` (e.g., `~/.gemini/GEMINI.md` in
|
||||
your user home directory).
|
||||
- Scope: Provides default instructions for all your projects.
|
||||
2. **Project root and 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.). The breadth of this
|
||||
search is limited to 200 directories by default, but can be configured
|
||||
with a `memoryDiscoveryMaxDirs` field in your `settings.json` file.
|
||||
- Scope: Allows for highly specific instructions relevant to a particular
|
||||
component, module, or subsection of your project.
|
||||
- **Concatenation and 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.
|
||||
- **Importing content:** You can modularize your context files by importing
|
||||
other Markdown files using the `@path/to/file.md` syntax. For more details,
|
||||
see the [Memory Import Processor documentation](../core/memport.md).
|
||||
- **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
|
||||
`.gemini/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 `.gemini/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
|
||||
}
|
||||
```
|
||||
@@ -15,19 +15,28 @@ available combinations.
|
||||
|
||||
#### Cursor Movement
|
||||
|
||||
| Action | Keys |
|
||||
| ----------------------------------------- | ---------------------- |
|
||||
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home` |
|
||||
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End` |
|
||||
| Action | Keys |
|
||||
| ------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home` |
|
||||
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End` |
|
||||
| Move the cursor one character to the left. | `Left Arrow (no Ctrl, no Cmd)`<br />`Ctrl + B` |
|
||||
| Move the cursor one character to the right. | `Right Arrow (no Ctrl, no Cmd)`<br />`Ctrl + F` |
|
||||
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Cmd + Left Arrow`<br />`Cmd + B` |
|
||||
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Cmd + Right Arrow`<br />`Cmd + F` |
|
||||
|
||||
#### Editing
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | ----------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Cmd + Backspace` |
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | -------------------------------------------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Cmd + Backspace`<br />`Ctrl + ""`<br />`Cmd + ""`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Cmd + Delete` |
|
||||
| Delete the character to the left. | `Backspace`<br />`""`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
|
||||
| Redo the most recent undone text edit. | `Ctrl + Shift + Z` |
|
||||
|
||||
#### Screen Control
|
||||
|
||||
@@ -53,7 +62,7 @@ available combinations.
|
||||
| Show the previous entry in history. | `Ctrl + P (no Shift)` |
|
||||
| Show the next entry in history. | `Ctrl + N (no Shift)` |
|
||||
| Start reverse search through history. | `Ctrl + R` |
|
||||
| Insert the selected reverse-search match. | `Enter (no Ctrl)` |
|
||||
| Submit the selected reverse-search match. | `Enter (no Ctrl)` |
|
||||
| Accept a suggestion while reverse searching. | `Tab` |
|
||||
|
||||
#### Navigation
|
||||
@@ -91,15 +100,18 @@ available combinations.
|
||||
|
||||
#### App Controls
|
||||
|
||||
| Action | Keys |
|
||||
| ----------------------------------------------------------------- | ---------- |
|
||||
| Toggle detailed error information. | `F12` |
|
||||
| Toggle the full TODO list. | `Ctrl + T` |
|
||||
| Toggle IDE context details. | `Ctrl + G` |
|
||||
| Toggle Markdown rendering. | `Cmd + M` |
|
||||
| Toggle copy mode when the terminal is using the alternate buffer. | `Ctrl + S` |
|
||||
| Expand a height-constrained response to show additional lines. | `Ctrl + S` |
|
||||
| Toggle focus between the shell and Gemini input. | `Ctrl + F` |
|
||||
| Action | Keys |
|
||||
| ----------------------------------------------------------------- | ---------------- |
|
||||
| Toggle detailed error information. | `F12` |
|
||||
| Toggle the full TODO list. | `Ctrl + T` |
|
||||
| Show IDE context details. | `Ctrl + G` |
|
||||
| Toggle Markdown rendering. | `Cmd + M` |
|
||||
| Toggle copy mode when the terminal is using the alternate buffer. | `Ctrl + S` |
|
||||
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
|
||||
| Toggle Auto Edit (auto-accept edits) mode. | `Shift + Tab` |
|
||||
| Expand a height-constrained response to show additional lines. | `Ctrl + S` |
|
||||
| Focus the shell input from the gemini input. | `Tab (no Shift)` |
|
||||
| Focus the Gemini input from the shell input. | `Tab` |
|
||||
|
||||
#### Session Control
|
||||
|
||||
@@ -112,30 +124,11 @@ available combinations.
|
||||
|
||||
## Additional context-specific shortcuts
|
||||
|
||||
- `Ctrl+Y`: Toggle YOLO (auto-approval) mode for tool calls.
|
||||
- `Shift+Tab`: Toggle Auto Edit (auto-accept edits) mode.
|
||||
- `Option+M` (macOS): Entering `µ` with Option+M also toggles Markdown
|
||||
rendering, matching `Cmd+M`.
|
||||
- `Option+B/F/M` (macOS only): Are interpreted as `Cmd+B/F/M` even if your
|
||||
terminal isn't configured to send Meta with Option.
|
||||
- `!` on an empty prompt: Enter or exit shell mode.
|
||||
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
|
||||
mode.
|
||||
- `Ctrl+Delete` / `Meta+Delete`: Delete the word to the right of the cursor.
|
||||
- `Ctrl+B` or `Left Arrow`: Move the cursor one character to the left while
|
||||
editing text.
|
||||
- `Ctrl+F` or `Right Arrow`: Move the cursor one character to the right; with an
|
||||
embedded shell attached, `Ctrl+F` still toggles focus.
|
||||
- `Ctrl+D` or `Delete`: Remove the character immediately to the right of the
|
||||
cursor.
|
||||
- `Ctrl+H` or `Backspace`: Remove the character immediately to the left of the
|
||||
cursor.
|
||||
- `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B`: Move one word to the left.
|
||||
- `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F`: Move one word to the
|
||||
right.
|
||||
- `Ctrl+W`: Delete the word to the left of the cursor (in addition to
|
||||
`Ctrl+Backspace` / `Cmd+Backspace`).
|
||||
- `Ctrl+Z` / `Ctrl+Shift+Z`: Undo or redo the most recent text edit.
|
||||
- `Meta+Enter`: Open the current input in an external editor (alias for
|
||||
`Ctrl+X`).
|
||||
- `Esc` pressed twice quickly: Clear the current input buffer.
|
||||
- `Up Arrow` / `Down Arrow`: When the cursor is at the top or bottom of a
|
||||
single-line input, navigate backward or forward through prompt history.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## Model routing
|
||||
# Model routing
|
||||
|
||||
Gemini CLI includes a model routing feature that automatically switches to a
|
||||
fallback model in case of a model failure. This feature is enabled by default
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ To enable Gemini 3 Pro and Gemini 3 Flash (if available), enable
|
||||
|
||||
You can also use the `--model` flag to specify a particular Gemini model on
|
||||
startup. For more details, refer to the
|
||||
[configuration documentation](./configuration.md).
|
||||
[configuration documentation](../get-started/configuration.md).
|
||||
|
||||
Changes to these settings will be applied to all subsequent interactions with
|
||||
Gemini CLI.
|
||||
|
||||
+69
-53
@@ -18,45 +18,50 @@ Note: Workspace settings override user settings.
|
||||
Here is a list of all the available settings, grouped by category and ordered as
|
||||
they appear in the UI.
|
||||
|
||||
<!-- SETTINGS-AUTOGEN:START -->
|
||||
|
||||
### General
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------- | ----------- |
|
||||
| Preview Features (e.g., models) | `general.previewFeatures` | Enable preview features (e.g., preview models). | `false` |
|
||||
| Vim Mode | `general.vimMode` | Enable Vim keybindings. | `false` |
|
||||
| Disable Auto Update | `general.disableAutoUpdate` | Disable automatic updates. | `false` |
|
||||
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
|
||||
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
||||
| Session Retention | `general.sessionRetention` | Settings for automatic session cleanup. This feature is disabled by default. | `undefined` |
|
||||
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup. | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------- | ---------------------------------- | ------------------------------------------------------------- | ------- |
|
||||
| Preview Features (e.g., models) | `general.previewFeatures` | Enable preview features (e.g., preview models). | `false` |
|
||||
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
|
||||
| Disable Auto Update | `general.disableAutoUpdate` | Disable automatic updates | `false` |
|
||||
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
|
||||
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
||||
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
|
||||
|
||||
### Output
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------- | --------------- | ------------------------------------------------------ | ------- |
|
||||
| Output Format | `output.format` | The format of the CLI output. Can be `text` or `json`. | `text` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------- | --------------- | ------------------------------------------------------ | -------- |
|
||||
| Output Format | `output.format` | The format of the CLI output. Can be `text` or `json`. | `"text"` |
|
||||
|
||||
### UI
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------ | ---------------------------------------- | -------------------------------------------------------------------- | ------- |
|
||||
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar. | `false` |
|
||||
| Show Status in Title | `ui.showStatusInTitle` | Show Gemini CLI status and thoughts in the terminal window title. | `false` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI. | `false` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner. | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
|
||||
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI. | `false` |
|
||||
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI. | `false` |
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `false` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Use Full Width | `ui.useFullWidth` | Use the entire width of the terminal for output. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `true` |
|
||||
| Disable Loading Phrases | `ui.accessibility.disableLoadingPhrases` | Disable loading phrases for accessibility. | `false` |
|
||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible. | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
|
||||
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
|
||||
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
|
||||
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
|
||||
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
|
||||
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Use Full Width | `ui.useFullWidth` | Use the entire width of the terminal for output. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
| Disable Loading Phrases | `ui.accessibility.disableLoadingPhrases` | Disable loading phrases for accessibility | `false` |
|
||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
||||
|
||||
### IDE
|
||||
|
||||
@@ -69,7 +74,7 @@ they appear in the UI.
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
|
||||
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
|
||||
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.2` |
|
||||
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
|
||||
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
|
||||
|
||||
### Context
|
||||
@@ -85,30 +90,41 @@ they appear in the UI.
|
||||
|
||||
### Tools
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| -------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
|
||||
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
|
||||
| Auto Accept | `tools.autoAccept` | Automatically accept and execute tool calls that are considered safe (e.g., read-only operations). | `false` |
|
||||
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
|
||||
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
|
||||
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `10000` |
|
||||
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `100` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
|
||||
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
|
||||
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
|
||||
| Auto Accept | `tools.autoAccept` | Automatically accept and execute tool calls that are considered safe (e.g., read-only operations). | `false` |
|
||||
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
|
||||
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
|
||||
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `4000000` |
|
||||
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `1000` |
|
||||
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `false` |
|
||||
|
||||
### Security
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ----------------------------- | ----------------------------------------------- | --------------------------------------------------------- | ------- |
|
||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
|
||||
| Allowed Environment Variables | `security.environmentVariableRedaction.allowed` | Environment variables to always allow (bypass redaction). | `[]` |
|
||||
| Blocked Environment Variables | `security.environmentVariableRedaction.blocked` | Environment variables to always redact. | `[]` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | ------- |
|
||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
|
||||
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
|
||||
|
||||
### Experimental
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ----------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ | ------- |
|
||||
| Enable Codebase Investigator | `experimental.codebaseInvestigatorSettings.enabled` | Enable the Codebase Investigator agent. | `true` |
|
||||
| Codebase Investigator Max Num Turns | `experimental.codebaseInvestigatorSettings.maxNumTurns` | Maximum number of turns for the Codebase Investigator agent. | `10` |
|
||||
| Agent Skills | `experimental.skills` | Enable Agent Skills (experimental). | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ----------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
|
||||
| Agent Skills | `experimental.skills` | Enable Agent Skills (experimental). | `false` |
|
||||
| Enable Codebase Investigator | `experimental.codebaseInvestigatorSettings.enabled` | Enable the Codebase Investigator agent. | `true` |
|
||||
| Codebase Investigator Max Num Turns | `experimental.codebaseInvestigatorSettings.maxNumTurns` | Maximum number of turns for the Codebase Investigator agent. | `10` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
|
||||
| Enable CLI Help Agent | `experimental.cliHelpAgentSettings.enabled` | Enable the CLI Help Agent. | `true` |
|
||||
|
||||
### Hooks
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------ | --------------------- | ------------------------------------------------ | ------- |
|
||||
| Hook Notifications | `hooks.notifications` | Show visual indicators when hooks are executing. | `true` |
|
||||
|
||||
<!-- SETTINGS-AUTOGEN:END -->
|
||||
|
||||
+35
-3
@@ -71,9 +71,26 @@ The `gemini skills` command provides management utilities:
|
||||
# List all discovered skills
|
||||
gemini skills list
|
||||
|
||||
# Enable/disable skills. Can use --scope to specify project or user
|
||||
# Install a skill from a Git repository, local directory, or zipped skill file (.skill)
|
||||
# Uses the user scope by default (~/.gemini/skills)
|
||||
gemini skills install https://github.com/user/repo.git
|
||||
gemini skills install /path/to/local/skill
|
||||
gemini skills install /path/to/local/my-expertise.skill
|
||||
|
||||
# Install a specific skill from a monorepo or subdirectory using --path
|
||||
gemini skills install https://github.com/my-org/my-skills.git --path skills/frontend-design
|
||||
|
||||
# Install to the workspace scope (.gemini/skills)
|
||||
gemini skills install /path/to/skill --scope workspace
|
||||
|
||||
# Uninstall a skill by name
|
||||
gemini skills uninstall my-expertise --scope workspace
|
||||
|
||||
# Enable a skill (globally)
|
||||
gemini skills enable my-expertise
|
||||
gemini skills disable my-expertise
|
||||
|
||||
# Disable a skill. Can use --scope to specify project or user (defaults to project)
|
||||
gemini skills disable my-expertise --scope project
|
||||
```
|
||||
|
||||
## Creating a Skill
|
||||
@@ -81,7 +98,20 @@ gemini skills disable my-expertise
|
||||
A skill is a directory containing a `SKILL.md` file at its root. This file uses
|
||||
YAML frontmatter for metadata and Markdown for instructions.
|
||||
|
||||
### Basic Structure
|
||||
### Folder Structure
|
||||
|
||||
Skills are self-contained directories. At a minimum, a skill requires a
|
||||
`SKILL.md` file, but can include other resources:
|
||||
|
||||
```text
|
||||
my-skill/
|
||||
├── SKILL.md (Required) Instructions and metadata
|
||||
├── scripts/ (Optional) Executable scripts/tools
|
||||
├── references/ (Optional) Static documentation and examples
|
||||
└── assets/ (Optional) Templates and binary resources
|
||||
```
|
||||
|
||||
### Basic Structure (SKILL.md)
|
||||
|
||||
```markdown
|
||||
---
|
||||
@@ -100,6 +130,8 @@ description: <what the skill does and when Gemini should use it>
|
||||
|
||||
### Example: Team Code Reviewer
|
||||
|
||||
Create `~/.gemini/skills/code-reviewer/SKILL.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-reviewer
|
||||
|
||||
@@ -8,8 +8,10 @@ Learn how to enable and setup OpenTelemetry for Gemini CLI.
|
||||
- [Configuration](#configuration)
|
||||
- [Google Cloud telemetry](#google-cloud-telemetry)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Authenticating with CLI Credentials](#authenticating-with-cli-credentials)
|
||||
- [Direct export (recommended)](#direct-export-recommended)
|
||||
- [Collector-based export (advanced)](#collector-based-export-advanced)
|
||||
- [Monitoring Dashboards](#monitoring-dashboards)
|
||||
- [Local telemetry](#local-telemetry)
|
||||
- [File-based output (recommended)](#file-based-output-recommended)
|
||||
- [Collector-based export (advanced)](#collector-based-export-advanced-1)
|
||||
@@ -213,6 +215,24 @@ forward data to Google Cloud.
|
||||
- Open `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log` to view local
|
||||
collector logs.
|
||||
|
||||
### Monitoring Dashboards
|
||||
|
||||
Gemini CLI provides a pre-configured
|
||||
[Google Cloud Monitoring](https://cloud.google.com/monitoring) dashboard to
|
||||
visualize your telemetry.
|
||||
|
||||
This dashboard can be found under **Google Cloud Monitoring Dashboard
|
||||
Templates** as "**Gemini CLI Monitoring**".
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
To learn more, check out this blog post:
|
||||
[Instant insights: Gemini CLI’s new pre-configured monitoring dashboards](https://cloud.google.com/blog/topics/developers-practitioners/instant-insights-gemini-clis-new-pre-configured-monitoring-dashboards/).
|
||||
|
||||
## Local telemetry
|
||||
|
||||
For local development and debugging, you can capture telemetry data locally:
|
||||
|
||||
@@ -83,7 +83,9 @@ The processor automatically detects and prevents circular imports:
|
||||
# file-a.md
|
||||
|
||||
@./file-b.md
|
||||
```
|
||||
|
||||
```markdown
|
||||
# file-b.md
|
||||
|
||||
@./file-a.md <!-- This will be detected and prevented -->
|
||||
|
||||
@@ -258,8 +258,9 @@ The Gemini CLI ships with a set of default policies to provide a safe
|
||||
out-of-the-box experience.
|
||||
|
||||
- **Read-only tools** (like `read_file`, `glob`) are generally **allowed**.
|
||||
- **Agent delegation** (like `delegate_to_agent`) is **allowed** (sub-agent
|
||||
actions are checked individually).
|
||||
- **Agent delegation** (like `delegate_to_agent`) defaults to **`ask_user`** to
|
||||
ensure remote agents can prompt for confirmation, but local sub-agent actions
|
||||
are executed silently and checked individually.
|
||||
- **Write tools** (like `write_file`, `run_shell_command`) default to
|
||||
**`ask_user`**.
|
||||
- In **`yolo`** mode, a high-priority rule allows all tools.
|
||||
|
||||
@@ -165,7 +165,7 @@ The file has the following structure:
|
||||
- `version`: The version of the extension.
|
||||
- `mcpServers`: A map of MCP servers to settings. The key is the name of the
|
||||
server, and the value is the server configuration. These servers will be
|
||||
loaded on startup just like MCP servers settingsd in a
|
||||
loaded on startup just like MCP servers settings in a
|
||||
[`settings.json` file](../get-started/configuration.md). If both an extension
|
||||
and a `settings.json` file settings an MCP server with the same name, the
|
||||
server defined in the `settings.json` file takes precedence.
|
||||
@@ -226,13 +226,13 @@ key. The value will be saved to a `.env` file in the extension's directory
|
||||
You can view a list of an extension's settings by running:
|
||||
|
||||
```
|
||||
gemini extensions settings list <extension name>
|
||||
gemini extensions list
|
||||
```
|
||||
|
||||
and you can update a given setting using:
|
||||
|
||||
```
|
||||
gemini extensions settings set <extension name> <setting name> [--scope <scope>]
|
||||
gemini extensions config <extension name> [setting name] [--scope <scope>]
|
||||
```
|
||||
|
||||
- `--scope`: The scope to set the setting in (`user` or `workspace`). This is
|
||||
@@ -263,6 +263,54 @@ Would provide these commands:
|
||||
- `/deploy` - Shows as `[gcp] Custom command from deploy.toml` in help
|
||||
- `/gcs:sync` - Shows as `[gcp] Custom command from sync.toml` in help
|
||||
|
||||
### Hooks
|
||||
|
||||
Extensions can provide [hooks](../hooks/index.md) to intercept and customize
|
||||
Gemini CLI behavior at specific lifecycle events. Hooks provided by an extension
|
||||
must be defined in a `hooks/hooks.json` file within the extension directory.
|
||||
|
||||
> [!IMPORTANT] Hooks are not defined directly in `gemini-extension.json`. The
|
||||
> CLI specifically looks for the `hooks/hooks.json` file.
|
||||
|
||||
#### Directory structure
|
||||
|
||||
```
|
||||
.gemini/extensions/my-extension/
|
||||
├── gemini-extension.json
|
||||
└── hooks/
|
||||
└── hooks.json
|
||||
```
|
||||
|
||||
#### `hooks/hooks.json` format
|
||||
|
||||
The `hooks.json` file contains a `hooks` object where keys are
|
||||
[event names](../hooks/reference.md#supported-events) and values are arrays of
|
||||
[hook definitions](../hooks/reference.md#hook-definition).
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"before_agent": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${extensionPath}/scripts/setup.js",
|
||||
"name": "Extension Setup"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Supported variables
|
||||
|
||||
Just like `gemini-extension.json`, the `hooks/hooks.json` file supports
|
||||
[variable substitution](#variables). This is particularly useful for referencing
|
||||
scripts within the extension directory using `${extensionPath}`.
|
||||
|
||||
### Conflict resolution
|
||||
|
||||
Extension commands have the lowest precedence. When a conflict occurs with user
|
||||
@@ -278,11 +326,12 @@ For example, if both a user and the `gcp` extension define a `deploy` command:
|
||||
- `/gcp.deploy` - Executes the extension's deploy command (marked with `[gcp]`
|
||||
tag)
|
||||
|
||||
## Variables
|
||||
### Variables
|
||||
|
||||
Gemini CLI extensions allow variable substitution in `gemini-extension.json`.
|
||||
This can be useful if e.g., you need the current directory to run an MCP server
|
||||
using `"cwd": "${extensionPath}${/}run.ts"`.
|
||||
Gemini CLI extensions allow variable substitution in both
|
||||
`gemini-extension.json` and `hooks/hooks.json`. This can be useful if e.g., you
|
||||
need the current directory to run an MCP server or hook script using
|
||||
`"cwd": "${extensionPath}${/}run.ts"`.
|
||||
|
||||
**Supported variables:**
|
||||
|
||||
@@ -291,3 +340,4 @@ using `"cwd": "${extensionPath}${/}run.ts"`.
|
||||
| `${extensionPath}` | The fully-qualified path of the extension in the user's filesystem e.g., '/Users/username/.gemini/extensions/example-extension'. This will not unwrap symlinks. |
|
||||
| `${workspacePath}` | The fully-qualified path of the current workspace. |
|
||||
| `${/} or ${pathSeparator}` | The path separator (differs per OS). |
|
||||
| `${process.execPath}` | The path to the Node.js binary executing the CLI. |
|
||||
|
||||
@@ -159,7 +159,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
#### `output`
|
||||
|
||||
- **`output.format`** (enum):
|
||||
- **Description:** The format of the CLI output.
|
||||
- **Description:** The format of the CLI output. Can be `text` or `json`.
|
||||
- **Default:** `"text"`
|
||||
- **Values:** `"text"`, `"json"`
|
||||
|
||||
@@ -180,10 +180,21 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`ui.showStatusInTitle`** (boolean):
|
||||
- **Description:** Show Gemini CLI status and thoughts in the terminal window
|
||||
title
|
||||
- **Description:** Show Gemini CLI model thoughts in the terminal window title
|
||||
during the working phase
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.dynamicWindowTitle`** (boolean):
|
||||
- **Description:** Update the terminal window title with current status icons
|
||||
(Ready: ◇, Action Required: ✋, Working: ✦)
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.showHomeDirectoryWarning`** (boolean):
|
||||
- **Description:** Show a warning when running Gemini CLI in the home
|
||||
directory.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`ui.hideTips`** (boolean):
|
||||
- **Description:** Hide helpful tips in the UI
|
||||
- **Default:** `false`
|
||||
@@ -269,7 +280,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
#### `ide`
|
||||
|
||||
- **`ide.enabled`** (boolean):
|
||||
- **Description:** Enable IDE integration mode
|
||||
- **Description:** Enable IDE integration mode.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -546,6 +557,14 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
used.
|
||||
- **Default:** `[]`
|
||||
|
||||
#### `agents`
|
||||
|
||||
- **`agents.overrides`** (object):
|
||||
- **Description:** Override settings for specific agents, e.g. to disable the
|
||||
agent, set a custom model config, or run config.
|
||||
- **Default:** `{}`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `context`
|
||||
|
||||
- **`context.fileName`** (string | string[]):
|
||||
@@ -573,12 +592,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`context.fileFiltering.respectGitIgnore`** (boolean):
|
||||
- **Description:** Respect .gitignore files when searching
|
||||
- **Description:** Respect .gitignore files when searching.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`context.fileFiltering.respectGeminiIgnore`** (boolean):
|
||||
- **Description:** Respect .geminiignore files when searching
|
||||
- **Description:** Respect .geminiignore files when searching.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -684,6 +703,13 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `1000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.disableLLMCorrection`** (boolean):
|
||||
- **Description:** Disable LLM-based error correction for edit tools. When
|
||||
enabled, tools will fail immediately if exact string matches are not found,
|
||||
instead of attempting to self-correct.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`tools.enableHooks`** (boolean):
|
||||
- **Description:** Enables the hooks system experiment. When disabled, the
|
||||
hooks system is completely deactivated regardless of other settings.
|
||||
@@ -804,6 +830,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionConfig`** (boolean):
|
||||
- **Description:** Enable requesting and fetching of extension settings.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionReloading`** (boolean):
|
||||
- **Description:** Enables extension loading/unloading within the CLI session.
|
||||
- **Default:** `false`
|
||||
@@ -851,9 +882,9 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
(useful for remote sessions).
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.introspectionAgentSettings.enabled`** (boolean):
|
||||
- **Description:** Enable the Introspection Agent.
|
||||
- **Default:** `false`
|
||||
- **`experimental.cliHelpAgentSettings.enabled`** (boolean):
|
||||
- **Description:** Enable the CLI Help Agent.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `skills`
|
||||
@@ -948,6 +979,10 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **`admin.mcp.enabled`** (boolean):
|
||||
- **Description:** If false, disallows MCP servers from being used.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`admin.skills.enabled`** (boolean):
|
||||
- **Description:** If false, disallows agent skills from being used.
|
||||
- **Default:** `true`
|
||||
<!-- SETTINGS-AUTOGEN:END -->
|
||||
|
||||
#### `mcpServers`
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
Note: This page will be replaced by [installation.md](installation.md).
|
||||
|
||||
# Gemini CLI installation, execution, and deployment
|
||||
|
||||
Install and run Gemini CLI. This document provides an overview of Gemini CLI's
|
||||
installation methods and deployment architecture.
|
||||
|
||||
## How to install and/or run Gemini CLI
|
||||
|
||||
There are several ways to run Gemini CLI. The recommended option depends on how
|
||||
you intend to use Gemini CLI.
|
||||
|
||||
- As a standard installation. This is the most straightforward method of using
|
||||
Gemini CLI.
|
||||
- In a sandbox. This method offers increased security and isolation.
|
||||
- From the source. This is recommended for contributors to the project.
|
||||
|
||||
### 1. Standard installation (recommended for standard 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. Run in a sandbox (Docker/Podman)
|
||||
|
||||
For security and isolation, Gemini CLI can be run inside a container. This is
|
||||
the default way that the CLI executes tools that might have side effects.
|
||||
|
||||
- **Directly from the registry:** You can run the published sandbox image
|
||||
directly. This is useful for environments where you only have Docker and want
|
||||
to run the CLI.
|
||||
```bash
|
||||
# Run the published sandbox image
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
|
||||
```
|
||||
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
|
||||
(using the standard installation described above), you can instruct it to run
|
||||
inside the sandbox container.
|
||||
```bash
|
||||
gemini --sandbox -y -p "your prompt here"
|
||||
```
|
||||
|
||||
### 3. Run from source (recommended for Gemini CLI contributors)
|
||||
|
||||
Contributors to the project will want to run the CLI directly from the source
|
||||
code.
|
||||
|
||||
- **Development mode:** This method provides hot-reloading and is useful for
|
||||
active development.
|
||||
```bash
|
||||
# From the root of the repository
|
||||
npm run start
|
||||
```
|
||||
- **Production-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.
|
||||
@@ -1,25 +1,10 @@
|
||||
# Gemini 3 Pro and Gemini 3 Flash on Gemini CLI
|
||||
|
||||
Gemini 3 Pro and Gemini 3 Flash are now available on Gemini CLI! Currently, most
|
||||
paid customers of Gemini CLI will have access to both Gemini 3 Pro and Gemini 3
|
||||
Flash, including the following subscribers:
|
||||
|
||||
- Google AI Pro and Google AI Ultra (excluding business customers).
|
||||
- Gemini Code Assist Standard and Enterprise (requires
|
||||
[administrative enablement](#administrator-instructions)).
|
||||
- Paid Gemini API and Vertex API key holders.
|
||||
|
||||
For free tier users:
|
||||
|
||||
- If you signed up for the waitlist, please check your email for details. We’ve
|
||||
onboarded everyone who signed up to the previously available waitlist.
|
||||
- If you were not on our waitlist, we’re rolling out additional access gradually
|
||||
to ensure the experience remains fast and reliable. Stay tuned for more
|
||||
details.
|
||||
Gemini 3 Pro and Gemini 3 Flash are available on Gemini CLI for all users!
|
||||
|
||||
## How to get started with Gemini 3 on Gemini CLI
|
||||
|
||||
Get started by upgrading Gemini CLI to the latest version (0.21.1):
|
||||
Get started by upgrading Gemini CLI to the latest version:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@latest
|
||||
|
||||
@@ -852,5 +852,5 @@ console.log(JSON.stringify(sanitizeOutput(hookOutput)));
|
||||
|
||||
- [Hooks Reference](index.md) - Complete API reference
|
||||
- [Writing Hooks](writing-hooks.md) - Tutorial and examples
|
||||
- [Configuration](../cli/configuration.md) - Gemini CLI settings
|
||||
- [Configuration](../get-started/configuration.md) - Gemini CLI settings
|
||||
- [Hooks Design Document](../hooks-design.md) - Technical architecture
|
||||
|
||||
+47
-11
@@ -4,6 +4,19 @@ Hooks are scripts or programs that Gemini CLI executes at specific points in the
|
||||
agentic loop, allowing you to intercept and customize behavior without modifying
|
||||
the CLI's source code.
|
||||
|
||||
> **Note: Hooks are currently an experimental feature.**
|
||||
>
|
||||
> To use hooks, you must explicitly enable them in your `settings.json`:
|
||||
>
|
||||
> ```json
|
||||
> {
|
||||
> "tools": { "enableHooks": true },
|
||||
> "hooks": { "enabled": true }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Both of these are needed in this experimental phase.
|
||||
|
||||
See [writing hooks guide](writing-hooks.md) for a tutorial on creating your
|
||||
first hook and a comprehensive example.
|
||||
|
||||
@@ -29,10 +42,10 @@ Gemini CLI waits for all matching hooks to complete before continuing.
|
||||
|
||||
## Security and Risks
|
||||
|
||||
> [!WARNING] **Hooks execute arbitrary code with your user privileges.**
|
||||
|
||||
By configuring hooks, you are explicitly allowing Gemini CLI to run shell
|
||||
commands on your machine. Malicious or poorly configured hooks can:
|
||||
> **Warning: Hooks execute arbitrary code with your user privileges.**
|
||||
>
|
||||
> By configuring hooks, you are explicitly allowing Gemini CLI to run shell
|
||||
> commands on your machine. Malicious or poorly configured hooks can:
|
||||
|
||||
- **Exfiltrate data**: Read sensitive files (`.env`, ssh keys) and send them to
|
||||
remote servers.
|
||||
@@ -46,6 +59,11 @@ project hook (identified by its name and command), but it is **your
|
||||
responsibility** to review these hooks (and any installed extensions) before
|
||||
trusting them.
|
||||
|
||||
> **Note:** Extension hooks are subject to a mandatory security warning and
|
||||
> consent flow during extension installation or update if hooks are detected.
|
||||
> You must explicitly approve the installation or update of any extension that
|
||||
> contains hooks.
|
||||
|
||||
See [Security Considerations](best-practices.md#using-hooks-securely) for a
|
||||
detailed threat model and mitigation strategies.
|
||||
|
||||
@@ -444,7 +462,8 @@ numbers run first):
|
||||
2. **User settings:** `~/.gemini/settings.json`
|
||||
3. **System settings:** `/etc/gemini-cli/settings.json`
|
||||
4. **Extensions:** Internal hooks defined by installed extensions (lowest
|
||||
priority)
|
||||
priority). See [Extensions documentation](../extensions/index.md#hooks) for
|
||||
details on how extensions define and configure hooks.
|
||||
|
||||
#### Deduplication and shadowing
|
||||
|
||||
@@ -514,14 +533,29 @@ Use the `/hooks panel` command to view all registered hooks:
|
||||
|
||||
This command displays:
|
||||
|
||||
- All active hooks organized by event
|
||||
- All configured hooks organized by event
|
||||
- Hook source (user, project, system)
|
||||
- Hook type (command or plugin)
|
||||
- Execution status and recent output
|
||||
- Individual hook status (enabled/disabled)
|
||||
|
||||
### Enable and disable hooks
|
||||
### Enable and disable all hooks at once
|
||||
|
||||
You can temporarily enable or disable individual hooks using commands:
|
||||
You can enable or disable all hooks at once using commands:
|
||||
|
||||
```bash
|
||||
/hooks enable-all
|
||||
/hooks disable-all
|
||||
```
|
||||
|
||||
These commands provide a shortcut to enable or disable all configured hooks
|
||||
without managing them individually. The `enable-all` command removes all hooks
|
||||
from the `hooks.disabled` array, while `disable-all` adds all configured hooks
|
||||
to the disabled list. Changes take effect immediately without requiring a
|
||||
restart.
|
||||
|
||||
### Enable and disable individual hooks
|
||||
|
||||
You can enable or disable individual hooks using commands:
|
||||
|
||||
```bash
|
||||
/hooks enable hook-name
|
||||
@@ -530,7 +564,8 @@ You can temporarily enable or disable individual hooks using commands:
|
||||
|
||||
These commands allow you to control hook execution without editing configuration
|
||||
files. The hook name should match the `name` field in your hook configuration.
|
||||
Changes made via these commands are persisted to your global User settings
|
||||
Changes made via these commands are persisted to your settings. The settings are
|
||||
saved to workspace scope if available, otherwise to your global user settings
|
||||
(`~/.gemini/settings.json`).
|
||||
|
||||
### Disabled hooks configuration
|
||||
@@ -683,5 +718,6 @@ matchers:
|
||||
- [Best Practices](best-practices.md) - Security, performance, and debugging
|
||||
- [Custom Commands](../cli/custom-commands.md) - Create reusable prompt
|
||||
shortcuts
|
||||
- [Configuration](../cli/configuration.md) - Gemini CLI configuration options
|
||||
- [Configuration](../get-started/configuration.md) - Gemini CLI configuration
|
||||
options
|
||||
- [Hooks Design Document](../hooks-design.md) - Technical architecture details
|
||||
|
||||
+19
-9
@@ -46,6 +46,16 @@ specific event.
|
||||
- `tool_input`: (`object`) The arguments passed to the tool.
|
||||
- `tool_response`: (`object`, **AfterTool only**) The raw output from the tool
|
||||
execution.
|
||||
- `mcp_context`: (`object`, **optional**) Present only for MCP tool invocations.
|
||||
Contains server identity information:
|
||||
- `server_name`: (`string`) The configured name of the MCP server.
|
||||
- `tool_name`: (`string`) The original tool name from the MCP server.
|
||||
- `command`: (`string`, optional) For stdio transport, the command used to
|
||||
start the server.
|
||||
- `args`: (`string[]`, optional) For stdio transport, the command arguments.
|
||||
- `cwd`: (`string`, optional) For stdio transport, the working directory.
|
||||
- `url`: (`string`, optional) For SSE/HTTP transport, the server URL.
|
||||
- `tcp`: (`string`, optional) For WebSocket transport, the TCP address.
|
||||
|
||||
#### Agent Events (`BeforeAgent`, `AfterAgent`)
|
||||
|
||||
@@ -84,15 +94,15 @@ If the hook exits with `0`, the CLI attempts to parse `stdout` as JSON.
|
||||
|
||||
### Common Output Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| :------------------- | :-------- | :----------------------------------------------------------------------- |
|
||||
| `decision` | `string` | One of: `allow`, `deny`, `block`, `ask`, `approve`. |
|
||||
| `reason` | `string` | Explanation shown to the **agent** when a decision is `deny` or `block`. |
|
||||
| `systemMessage` | `string` | Message displayed to the **user** in the CLI terminal. |
|
||||
| `continue` | `boolean` | If `false`, immediately terminates the agent loop for this turn. |
|
||||
| `stopReason` | `string` | Message shown to the user when `continue` is `false`. |
|
||||
| `suppressOutput` | `boolean` | If `true`, the hook execution is hidden from the CLI transcript. |
|
||||
| `hookSpecificOutput` | `object` | Container for event-specific data (see below). |
|
||||
| Field | Type | Description |
|
||||
| :------------------- | :-------- | :------------------------------------------------------------------------------------- |
|
||||
| `decision` | `string` | One of: `allow`, `deny`, `block`, `ask`, `approve`. |
|
||||
| `reason` | `string` | Explanation shown to the **agent** when a decision is `deny` or `block`. |
|
||||
| `systemMessage` | `string` | Message displayed in Gemini CLI terminal to provide warning or context to the **user** |
|
||||
| `continue` | `boolean` | If `false`, immediately terminates the agent loop for this turn. |
|
||||
| `stopReason` | `string` | Message shown to the user when `continue` is `false`. |
|
||||
| `suppressOutput` | `boolean` | If `true`, the hook execution is hidden from the CLI transcript. |
|
||||
| `hookSpecificOutput` | `object` | Container for event-specific data (see below). |
|
||||
|
||||
### `hookSpecificOutput` Reference
|
||||
|
||||
|
||||
@@ -1018,9 +1018,27 @@ const SECRET_PATTERNS = [
|
||||
];
|
||||
```
|
||||
|
||||
## Packaging as an extension
|
||||
|
||||
While project-level hooks are great for specific repositories, you might want to
|
||||
share your hooks across multiple projects or with other users. You can do this
|
||||
by packaging your hooks as a [Gemini CLI extension](../extensions/index.md).
|
||||
|
||||
Packaging as an extension provides:
|
||||
|
||||
- **Easy distribution:** Share hooks via a git repository or GitHub release.
|
||||
- **Centralized management:** Install, update, and disable hooks using
|
||||
`gemini extensions` commands.
|
||||
- **Version control:** Manage hook versions separately from your project code.
|
||||
- **Variable substitution:** Use `${extensionPath}` and `${process.execPath}`
|
||||
for portable, cross-platform scripts.
|
||||
|
||||
To package hooks as an extension, follow the
|
||||
[extensions hook documentation](../extensions/index.md#hooks).
|
||||
|
||||
## Learn more
|
||||
|
||||
- [Hooks Reference](index.md) - Complete API reference and configuration
|
||||
- [Best Practices](best-practices.md) - Security, performance, and debugging
|
||||
- [Configuration](../cli/configuration.md) - Gemini CLI settings
|
||||
- [Configuration](../get-started/configuration.md) - Gemini CLI settings
|
||||
- [Custom Commands](../cli/custom-commands.md) - Create custom commands
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Dressing Room, which is Google's system for managing NPM packages in the
|
||||
`@google/**` namespace. The packages are all named `@google/**`.
|
||||
|
||||
More information can be found about these systems in the
|
||||
[maintainer repo guide](https://github.com/google-gemini/maintainers-gemini-cli/blob/main/npm.md)
|
||||
[NPM Package Overview](npm.md)
|
||||
|
||||
### Package scopes
|
||||
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ user input, such as text editors (`vim`, `nano`), terminal-based UIs (`htop`),
|
||||
and interactive version control operations (`git rebase -i`).
|
||||
|
||||
When an interactive command is running, you can send input to it from the Gemini
|
||||
CLI. To focus on the interactive shell, press `ctrl+f`. The terminal output,
|
||||
CLI. To focus on the interactive shell, press `Tab`. The terminal output,
|
||||
including complex TUIs, will be rendered correctly.
|
||||
|
||||
## Important notes
|
||||
|
||||
@@ -43,9 +43,15 @@ topics on:
|
||||
- **Cause:** You may be on a corporate network with a firewall that intercepts
|
||||
and inspects SSL/TLS traffic. This often requires a custom root CA
|
||||
certificate to be trusted by Node.js.
|
||||
- **Solution:** Set the `NODE_EXTRA_CA_CERTS` environment variable to the
|
||||
absolute path of your corporate root CA certificate file.
|
||||
- Example: `export NODE_EXTRA_CA_CERTS=/path/to/your/corporate-ca.crt`
|
||||
- **Solution:** First try setting `NODE_USE_SYSTEM_CA`; if that does not
|
||||
resolve the issue, set `NODE_EXTRA_CA_CERTS`.
|
||||
- Set the `NODE_USE_SYSTEM_CA=1` environment variable to tell Node.js to use
|
||||
the operating system's native certificate store (where corporate
|
||||
certificates are typically already installed).
|
||||
- Example: `export NODE_USE_SYSTEM_CA=1`
|
||||
- Set the `NODE_EXTRA_CA_CERTS` environment variable to the absolute path of
|
||||
your corporate root CA certificate file.
|
||||
- Example: `export NODE_EXTRA_CA_CERTS=/path/to/your/corporate-ca.crt`
|
||||
|
||||
## Common error messages and solutions
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ const external = [
|
||||
'@lydell/node-pty-linux-x64',
|
||||
'@lydell/node-pty-win32-arm64',
|
||||
'@lydell/node-pty-win32-x64',
|
||||
'keytar',
|
||||
];
|
||||
|
||||
const baseConfig = {
|
||||
|
||||
@@ -35,6 +35,8 @@ export default tseslint.config(
|
||||
'package/bundle/**',
|
||||
'.integration-tests/**',
|
||||
'dist/**',
|
||||
'evals/**',
|
||||
'packages/test-utils/**',
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
@@ -169,6 +171,38 @@ export default tseslint.config(
|
||||
'@typescript-eslint/await-thenable': ['error'],
|
||||
'@typescript-eslint/no-floating-promises': ['error'],
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': ['error'],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'node:os',
|
||||
importNames: ['homedir', 'tmpdir'],
|
||||
message:
|
||||
'Please use the helpers from @google/gemini-cli-core instead of node:os homedir()/tmpdir() to ensure strict environment isolation.',
|
||||
},
|
||||
{
|
||||
name: 'os',
|
||||
importNames: ['homedir', 'tmpdir'],
|
||||
message:
|
||||
'Please use the helpers from @google/gemini-cli-core instead of os homedir()/tmpdir() to ensure strict environment isolation.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Allow os.homedir() in tests and paths.ts where it is used to implement the helper
|
||||
files: [
|
||||
'**/*.test.ts',
|
||||
'**/*.test.tsx',
|
||||
'packages/core/src/utils/paths.ts',
|
||||
'packages/test-utils/src/**/*.ts',
|
||||
'scripts/**/*.js',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-imports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
# Behavioral Evals
|
||||
|
||||
Behavioral evaluations (evals) are tests designed to validate the agent's
|
||||
behavior in response to specific prompts. They serve as a critical feedback loop
|
||||
for changes to system prompts, tool definitions, and other model-steering
|
||||
mechanisms.
|
||||
|
||||
## Why Behavioral Evals?
|
||||
|
||||
Unlike traditional **integration tests** which verify that the system functions
|
||||
correctly (e.g., "does the file writer actually write to disk?"), behavioral
|
||||
evals verify that the model _chooses_ to take the correct action (e.g., "does
|
||||
the model decide to write to disk when asked to save code?").
|
||||
|
||||
They are also distinct from broad **industry benchmarks** (like SWE-bench).
|
||||
While benchmarks measure general capabilities across complex challenges, our
|
||||
behavioral evals focus on specific, granular behaviors relevant to the Gemini
|
||||
CLI's features.
|
||||
|
||||
### Key Characteristics
|
||||
|
||||
- **Feedback Loop**: They help us understand how changes to prompts or tools
|
||||
affect the model's decision-making.
|
||||
- _Did a change to the system prompt make the model less likely to use tool
|
||||
X?_
|
||||
- _Did a new tool definition confuse the model?_
|
||||
- **Regression Testing**: They prevent regressions in model steering.
|
||||
- **Non-Determinism**: Unlike unit tests, LLM behavior can be non-deterministic.
|
||||
We distinguish between behaviors that should be robust (`ALWAYS_PASSES`) and
|
||||
those that are generally reliable but might occasionally vary
|
||||
(`USUALLY_PASSES`).
|
||||
|
||||
## Creating an Evaluation
|
||||
|
||||
Evaluations are located in the `evals` directory. Each evaluation is a Vitest
|
||||
test file that uses the `evalTest` function from `evals/test-helper.ts`.
|
||||
|
||||
### `evalTest`
|
||||
|
||||
The `evalTest` function is a helper that runs a single evaluation case. It takes
|
||||
two arguments:
|
||||
|
||||
1. `policy`: The consistency expectation for this test (`'ALWAYS_PASSES'` or
|
||||
`'USUALLY_PASSES'`).
|
||||
2. `evalCase`: An object defining the test case.
|
||||
|
||||
#### Policies
|
||||
|
||||
Policies control how strictly a test is validated. Tests should generally use
|
||||
the ALWAYS_PASSES policy to offer the strictest guarantees.
|
||||
|
||||
USUALLY_PASSES exists to enable assertion of less consistent or aspirational
|
||||
behaviors.
|
||||
|
||||
- `ALWAYS_PASSES`: Tests expected to pass 100% of the time. These are typically
|
||||
trivial and test basic functionality. These run in every CI.
|
||||
- `USUALLY_PASSES`: Tests expected to pass most of the time but may have some
|
||||
flakiness due to non-deterministic behaviors. These are run nightly and used
|
||||
to track the health of the product from build to build.
|
||||
|
||||
#### `EvalCase` Properties
|
||||
|
||||
- `name`: The name of the evaluation case.
|
||||
- `prompt`: The prompt to send to the model.
|
||||
- `params`: An optional object with parameters to pass to the test rig (e.g.,
|
||||
settings).
|
||||
- `assert`: An async function that takes the test rig and the result of the run
|
||||
and asserts that the result is correct.
|
||||
- `log`: An optional boolean that, if set to `true`, will log the tool calls to
|
||||
a file in the `evals/logs` directory.
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('my_feature', () => {
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: 'should do something',
|
||||
prompt: 'do it',
|
||||
assert: async (rig, result) => {
|
||||
// assertions
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Running Evaluations
|
||||
|
||||
### Always Passing Evals
|
||||
|
||||
To run the evaluations that are expected to always pass (CI safe):
|
||||
|
||||
```bash
|
||||
npm run test:always_passing_evals
|
||||
```
|
||||
|
||||
### All Evals
|
||||
|
||||
To run all evaluations, including those that may be flaky ("usually passes"):
|
||||
|
||||
```bash
|
||||
npm run test:all_evals
|
||||
```
|
||||
|
||||
This command sets the `RUN_EVALS` environment variable to `1`, which enables the
|
||||
`USUALLY_PASSES` tests.
|
||||
|
||||
## Reporting
|
||||
|
||||
Results for evaluations are available on GitHub Actions:
|
||||
|
||||
- **CI Evals**: Included in the
|
||||
[E2E (Chained)](https://github.com/google-gemini/gemini-cli/actions/workflows/chained_e2e.yml)
|
||||
workflow. These must pass 100% for every PR.
|
||||
- **Nightly Evals**: Run daily via the
|
||||
[Evals: Nightly](https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml)
|
||||
workflow. These track the long-term health and stability of model steering.
|
||||
|
||||
### Nightly Report Format
|
||||
|
||||
The nightly workflow executes the full evaluation suite multiple times
|
||||
(currently 3 attempts) to account for non-determinism. These results are
|
||||
aggregated into a **Nightly Summary** attached to the workflow run.
|
||||
|
||||
#### How to interpret the report:
|
||||
|
||||
- **Pass Rate (%)**: Each cell represents the percentage of successful runs for
|
||||
a specific test in that workflow instance.
|
||||
- **History**: The table shows the pass rates for the last 10 nightly runs,
|
||||
allowing you to identify if a model's behavior is trending towards
|
||||
instability.
|
||||
- **Total Pass Rate**: An aggregate metric of all evaluations run in that batch.
|
||||
|
||||
A significant drop in the pass rate for a `USUALLY_PASSES` test—even if it
|
||||
doesn't drop to 0%—often indicates that a recent change to a system prompt or
|
||||
tool definition has made the model's behavior less reliable.
|
||||
|
||||
You may be able to investigate the regression using Gemini CLI by giving it the
|
||||
link to the runs before and after the change and the name of the test and asking
|
||||
it to investigate what changes may have impacted the test.
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
import { validateModelOutput } from '../integration-tests/test-helper.js';
|
||||
|
||||
describe('save_memory', () => {
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
name: 'should be able to save to memory',
|
||||
params: {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
},
|
||||
prompt: `remember that my favorite color is blue.
|
||||
|
||||
what is my favorite color? tell me that and surround it with $ symbol`,
|
||||
assert: async (rig, result) => {
|
||||
const foundToolCall = await rig.waitForToolCall('save_memory');
|
||||
expect(
|
||||
foundToolCall,
|
||||
'Expected to find a save_memory tool call',
|
||||
).toBeTruthy();
|
||||
|
||||
validateModelOutput(result, 'blue', 'Save memory test');
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import { TestRig } from '@google/gemini-cli-test-utils';
|
||||
|
||||
export * from '@google/gemini-cli-test-utils';
|
||||
|
||||
// Indicates the consistency expectation for this test.
|
||||
// - ALWAYS_PASSES - Means that the test is expected to pass 100% of the time. These
|
||||
// These tests are typically trivial and test basic functionality with unambiguous
|
||||
// prompts. For example: "call save_memory to remember foo" should be fairly reliable.
|
||||
// These are the first line of defense against regressions in key behaviors and run in
|
||||
// every CI. You can run these locally with 'npm run test:always_passing_evals'.
|
||||
//
|
||||
// - USUALLY_PASSES - Means that the test is expected to pass most of the time but
|
||||
// may have some flakiness as a result of relying on non-deterministic prompted
|
||||
// behaviors and/or ambiguous prompts or complex tasks.
|
||||
// For example: "Please do build changes until the very end" --> ambiguous whether
|
||||
// the agent should add to memory without more explicit system prompt or user
|
||||
// instructions. There are many more of these tests and they may pass less consistently.
|
||||
// The pass/fail trendline of this set of tests can be used as a general measure
|
||||
// of product quality. You can run these locally with 'npm run test:all_evals'.
|
||||
// This may take a really long time and is not recommended.
|
||||
export type EvalPolicy = 'ALWAYS_PASSES' | 'USUALLY_PASSES';
|
||||
|
||||
export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
const fn = async () => {
|
||||
const rig = new TestRig();
|
||||
try {
|
||||
await rig.setup(evalCase.name, evalCase.params);
|
||||
const result = await rig.run({ args: evalCase.prompt });
|
||||
await evalCase.assert(rig, result);
|
||||
} finally {
|
||||
await logToFile(
|
||||
evalCase.name,
|
||||
JSON.stringify(rig.readToolLogs(), null, 2),
|
||||
);
|
||||
await rig.cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
if (policy === 'USUALLY_PASSES' && !process.env.RUN_EVALS) {
|
||||
it.skip(evalCase.name, fn);
|
||||
} else {
|
||||
it(evalCase.name, fn);
|
||||
}
|
||||
}
|
||||
|
||||
export interface EvalCase {
|
||||
name: string;
|
||||
params?: Record<string, any>;
|
||||
prompt: string;
|
||||
assert: (rig: TestRig, result: string) => Promise<void>;
|
||||
}
|
||||
|
||||
async function logToFile(name: string, content: string) {
|
||||
const logDir = 'evals/logs';
|
||||
await fs.promises.mkdir(logDir, { recursive: true });
|
||||
const sanitizedName = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
const logFile = `${logDir}/${sanitizedName}.log`;
|
||||
await fs.promises.writeFile(logFile, content);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 300000, // 5 minutes
|
||||
reporters: ['default', 'json'],
|
||||
outputFile: {
|
||||
json: 'evals/logs/report.json',
|
||||
},
|
||||
include: ['**/*.eval.ts'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The capital of France is Paris."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":7,"candidatesTokenCount":7,"totalTokenCount":14,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7}]}}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Hello! How can I help you today?"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":9,"totalTokenCount":14,"promptTokensDetails":[{"modality":"TEXT","tokenCount":5}]}}]}
|
||||
@@ -14,7 +14,6 @@ describe('JSON output', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('json-output-test');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -22,6 +21,12 @@ describe('JSON output', () => {
|
||||
});
|
||||
|
||||
it('should return a valid JSON with response and stats', async () => {
|
||||
await rig.setup('json-output-france', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'json-output.france.responses',
|
||||
),
|
||||
});
|
||||
const result = await rig.run({
|
||||
args: ['What is the capital of France?', '--output-format', 'json'],
|
||||
});
|
||||
@@ -36,6 +41,12 @@ describe('JSON output', () => {
|
||||
});
|
||||
|
||||
it('should return a valid JSON with a session ID', async () => {
|
||||
await rig.setup('json-output-session-id', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'json-output.session-id.responses',
|
||||
),
|
||||
});
|
||||
const result = await rig.run({
|
||||
args: ['Hello', '--output-format', 'json'],
|
||||
});
|
||||
@@ -47,7 +58,6 @@ describe('JSON output', () => {
|
||||
});
|
||||
|
||||
it('should return a JSON error for sd auth mismatch before running', async () => {
|
||||
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
|
||||
await rig.setup('json-output-auth-mismatch', {
|
||||
settings: {
|
||||
security: {
|
||||
@@ -58,12 +68,13 @@ describe('JSON output', () => {
|
||||
|
||||
let thrown: Error | undefined;
|
||||
try {
|
||||
await rig.run({ args: ['Hello', '--output-format', 'json'] });
|
||||
await rig.run({
|
||||
args: ['Hello', '--output-format', 'json'],
|
||||
env: { GOOGLE_GENAI_USE_GCA: 'true' },
|
||||
});
|
||||
expect.fail('Expected process to exit with error');
|
||||
} catch (e) {
|
||||
thrown = e as Error;
|
||||
} finally {
|
||||
delete process.env['GOOGLE_GENAI_USE_GCA'];
|
||||
}
|
||||
|
||||
expect(thrown).toBeDefined();
|
||||
@@ -102,7 +113,7 @@ describe('JSON output', () => {
|
||||
});
|
||||
|
||||
it('should not exit on tool errors and allow model to self-correct in JSON mode', async () => {
|
||||
rig.setup('json-output-error', {
|
||||
await rig.setup('json-output-error', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'json-output.error.responses',
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||
|
||||
describe('save_memory', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should be able to save to memory', async () => {
|
||||
await rig.setup('should be able to save to memory', {
|
||||
settings: { tools: { core: ['save_memory'] } },
|
||||
});
|
||||
|
||||
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({ args: prompt });
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('save_memory');
|
||||
|
||||
// Add debugging information
|
||||
if (!foundToolCall || !result.toLowerCase().includes('blue')) {
|
||||
const allTools = printDebugInfo(rig, result, {
|
||||
'Found tool call': foundToolCall,
|
||||
'Contains blue': result.toLowerCase().includes('blue'),
|
||||
});
|
||||
|
||||
console.error(
|
||||
'Memory tool calls:',
|
||||
allTools
|
||||
.filter((t) => t.toolRequest.name === 'save_memory')
|
||||
.map((t) => t.toolRequest.args),
|
||||
);
|
||||
}
|
||||
|
||||
expect(
|
||||
foundToolCall,
|
||||
'Expected to find a save_memory tool call',
|
||||
).toBeTruthy();
|
||||
|
||||
// Validate model output - will throw if no output, warn if missing expected content
|
||||
validateModelOutput(result, 'blue', 'Save memory test');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+86
-167
File diff suppressed because it is too large
Load Diff
+8
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.24.0-nightly.20251227.37be16243",
|
||||
"version": "0.25.2",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.24.0-nightly.20251227.37be16243"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.25.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
@@ -41,12 +41,14 @@
|
||||
"test": "npm run test --workspaces --if-present",
|
||||
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts",
|
||||
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
|
||||
"test:always_passing_evals": "vitest run --config evals/vitest.config.ts",
|
||||
"test:all_evals": "cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts",
|
||||
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
|
||||
"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": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests",
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests && eslint scripts",
|
||||
"lint": "eslint . --cache",
|
||||
"lint:fix": "eslint . --fix --ext .ts,.tsx && eslint integration-tests --fix && eslint scripts --fix && npm run format",
|
||||
"lint:ci": "npm run lint:all",
|
||||
"lint:all": "node scripts/lint.js",
|
||||
@@ -62,7 +64,7 @@
|
||||
"pre-commit": "node scripts/pre-commit.js"
|
||||
},
|
||||
"overrides": {
|
||||
"ink": "npm:@jrichman/ink@6.4.6",
|
||||
"ink": "npm:@jrichman/ink@6.4.7",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
@@ -121,7 +123,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.4.6",
|
||||
"ink": "npm:@jrichman/ink@6.4.7",
|
||||
"latest-version": "^9.0.0",
|
||||
"simple-git": "^3.28.0"
|
||||
},
|
||||
@@ -132,6 +134,7 @@
|
||||
"@lydell/node-pty-linux-x64": "1.1.0",
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0",
|
||||
"keytar": "^7.9.0",
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.24.0-nightly.20251227.37be16243",
|
||||
"version": "0.25.2",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -7,53 +7,79 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { Command } from './types.js';
|
||||
|
||||
describe('CommandRegistry', () => {
|
||||
const mockListExtensionsCommandInstance: Command = {
|
||||
const {
|
||||
mockExtensionsCommand,
|
||||
mockListExtensionsCommand,
|
||||
mockExtensionsCommandInstance,
|
||||
mockListExtensionsCommandInstance,
|
||||
} = vi.hoisted(() => {
|
||||
const listInstance: Command = {
|
||||
name: 'extensions list',
|
||||
description: 'Lists all installed extensions.',
|
||||
execute: vi.fn(),
|
||||
};
|
||||
const mockListExtensionsCommand = vi.fn(
|
||||
() => mockListExtensionsCommandInstance,
|
||||
);
|
||||
|
||||
const mockExtensionsCommandInstance: Command = {
|
||||
const extInstance: Command = {
|
||||
name: 'extensions',
|
||||
description: 'Manage extensions.',
|
||||
execute: vi.fn(),
|
||||
subCommands: [mockListExtensionsCommandInstance],
|
||||
subCommands: [listInstance],
|
||||
};
|
||||
const mockExtensionsCommand = vi.fn(() => mockExtensionsCommandInstance);
|
||||
|
||||
return {
|
||||
mockListExtensionsCommandInstance: listInstance,
|
||||
mockExtensionsCommandInstance: extInstance,
|
||||
mockExtensionsCommand: vi.fn(() => extInstance),
|
||||
mockListExtensionsCommand: vi.fn(() => listInstance),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./extensions.js', () => ({
|
||||
ExtensionsCommand: mockExtensionsCommand,
|
||||
ListExtensionsCommand: mockListExtensionsCommand,
|
||||
}));
|
||||
|
||||
vi.mock('./init.js', () => ({
|
||||
InitCommand: vi.fn(() => ({
|
||||
name: 'init',
|
||||
description: 'Initializes the server.',
|
||||
execute: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('./restore.js', () => ({
|
||||
RestoreCommand: vi.fn(() => ({
|
||||
name: 'restore',
|
||||
description: 'Restores the server.',
|
||||
execute: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
import { commandRegistry } from './command-registry.js';
|
||||
|
||||
describe('CommandRegistry', () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock('./extensions.js', () => ({
|
||||
ExtensionsCommand: mockExtensionsCommand,
|
||||
ListExtensionsCommand: mockListExtensionsCommand,
|
||||
}));
|
||||
vi.clearAllMocks();
|
||||
commandRegistry.initialize();
|
||||
});
|
||||
|
||||
it('should register ExtensionsCommand on initialization', async () => {
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
expect(mockExtensionsCommand).toHaveBeenCalled();
|
||||
const command = commandRegistry.get('extensions');
|
||||
expect(command).toBe(mockExtensionsCommandInstance);
|
||||
});
|
||||
}, 20000);
|
||||
|
||||
it('should register sub commands on initialization', async () => {
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
const command = commandRegistry.get('extensions list');
|
||||
expect(command).toBe(mockListExtensionsCommandInstance);
|
||||
});
|
||||
|
||||
it('get() should return undefined for a non-existent command', async () => {
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
const command = commandRegistry.get('non-existent');
|
||||
expect(command).toBeUndefined();
|
||||
});
|
||||
|
||||
it('register() should register a new command', async () => {
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
const mockCommand: Command = {
|
||||
name: 'test-command',
|
||||
description: '',
|
||||
@@ -65,7 +91,6 @@ describe('CommandRegistry', () => {
|
||||
});
|
||||
|
||||
it('register() should register a nested command', async () => {
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
const mockSubSubCommand: Command = {
|
||||
name: 'test-command-sub-sub',
|
||||
description: '',
|
||||
@@ -95,8 +120,8 @@ describe('CommandRegistry', () => {
|
||||
});
|
||||
|
||||
it('register() should not enter an infinite loop with a cyclic command', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { commandRegistry } = await import('./command-registry.js');
|
||||
const { debugLogger } = await import('@google/gemini-cli-core');
|
||||
const warnSpy = vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
|
||||
const mockCommand: Command = {
|
||||
name: 'cyclic-command',
|
||||
description: '',
|
||||
@@ -112,7 +137,6 @@ describe('CommandRegistry', () => {
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'Command cyclic-command already registered. Skipping.',
|
||||
);
|
||||
// If the test finishes, it means we didn't get into an infinite loop.
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,19 +4,26 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { MemoryCommand } from './memory.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionsCommand } from './extensions.js';
|
||||
import { InitCommand } from './init.js';
|
||||
import { RestoreCommand } from './restore.js';
|
||||
import type { Command } from './types.js';
|
||||
|
||||
class CommandRegistry {
|
||||
export class CommandRegistry {
|
||||
private readonly commands = new Map<string, Command>();
|
||||
|
||||
constructor() {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.commands.clear();
|
||||
this.register(new ExtensionsCommand());
|
||||
this.register(new RestoreCommand());
|
||||
this.register(new InitCommand());
|
||||
this.register(new MemoryCommand());
|
||||
}
|
||||
|
||||
register(command: Command) {
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
addMemory,
|
||||
listMemoryFiles,
|
||||
refreshMemory,
|
||||
showMemory,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
AddMemoryCommand,
|
||||
ListMemoryCommand,
|
||||
MemoryCommand,
|
||||
RefreshMemoryCommand,
|
||||
ShowMemoryCommand,
|
||||
} from './memory.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
import type {
|
||||
AnyDeclarativeTool,
|
||||
Config,
|
||||
ToolRegistry,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the core functions
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
showMemory: vi.fn(),
|
||||
refreshMemory: vi.fn(),
|
||||
listMemoryFiles: vi.fn(),
|
||||
addMemory: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockShowMemory = vi.mocked(showMemory);
|
||||
const mockRefreshMemory = vi.mocked(refreshMemory);
|
||||
const mockListMemoryFiles = vi.mocked(listMemoryFiles);
|
||||
const mockAddMemory = vi.mocked(addMemory);
|
||||
|
||||
describe('a2a-server memory commands', () => {
|
||||
let mockContext: CommandContext;
|
||||
let mockConfig: Config;
|
||||
let mockToolRegistry: ToolRegistry;
|
||||
let mockSaveMemoryTool: AnyDeclarativeTool;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSaveMemoryTool = {
|
||||
name: 'save_memory',
|
||||
description: 'Saves memory',
|
||||
buildAndExecute: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as AnyDeclarativeTool;
|
||||
|
||||
mockToolRegistry = {
|
||||
getTool: vi.fn(),
|
||||
} as unknown as ToolRegistry;
|
||||
|
||||
mockConfig = {
|
||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||
} as unknown as Config;
|
||||
|
||||
mockContext = {
|
||||
config: mockConfig,
|
||||
};
|
||||
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockSaveMemoryTool);
|
||||
});
|
||||
|
||||
describe('MemoryCommand', () => {
|
||||
it('delegates to ShowMemoryCommand', async () => {
|
||||
const command = new MemoryCommand();
|
||||
mockShowMemory.mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'showing memory',
|
||||
});
|
||||
const response = await command.execute(mockContext, []);
|
||||
expect(response.data).toBe('showing memory');
|
||||
expect(mockShowMemory).toHaveBeenCalledWith(mockContext.config);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShowMemoryCommand', () => {
|
||||
it('executes showMemory and returns the content', async () => {
|
||||
const command = new ShowMemoryCommand();
|
||||
mockShowMemory.mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'test memory content',
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, []);
|
||||
|
||||
expect(mockShowMemory).toHaveBeenCalledWith(mockContext.config);
|
||||
expect(response.name).toBe('memory show');
|
||||
expect(response.data).toBe('test memory content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RefreshMemoryCommand', () => {
|
||||
it('executes refreshMemory and returns the content', async () => {
|
||||
const command = new RefreshMemoryCommand();
|
||||
mockRefreshMemory.mockResolvedValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'memory refreshed',
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, []);
|
||||
|
||||
expect(mockRefreshMemory).toHaveBeenCalledWith(mockContext.config);
|
||||
expect(response.name).toBe('memory refresh');
|
||||
expect(response.data).toBe('memory refreshed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ListMemoryCommand', () => {
|
||||
it('executes listMemoryFiles and returns the content', async () => {
|
||||
const command = new ListMemoryCommand();
|
||||
mockListMemoryFiles.mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'file1.md\nfile2.md',
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, []);
|
||||
|
||||
expect(mockListMemoryFiles).toHaveBeenCalledWith(mockContext.config);
|
||||
expect(response.name).toBe('memory list');
|
||||
expect(response.data).toBe('file1.md\nfile2.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AddMemoryCommand', () => {
|
||||
it('returns message content if addMemory returns a message', async () => {
|
||||
const command = new AddMemoryCommand();
|
||||
mockAddMemory.mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'error message',
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, []);
|
||||
|
||||
expect(mockAddMemory).toHaveBeenCalledWith('');
|
||||
expect(response.name).toBe('memory add');
|
||||
expect(response.data).toBe('error message');
|
||||
});
|
||||
|
||||
it('executes the save_memory tool if found', async () => {
|
||||
const command = new AddMemoryCommand();
|
||||
const fact = 'this is a new fact';
|
||||
mockAddMemory.mockReturnValue({
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact },
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, [
|
||||
'this',
|
||||
'is',
|
||||
'a',
|
||||
'new',
|
||||
'fact',
|
||||
]);
|
||||
|
||||
expect(mockAddMemory).toHaveBeenCalledWith(fact);
|
||||
expect(mockConfig.getToolRegistry).toHaveBeenCalled();
|
||||
expect(mockToolRegistry.getTool).toHaveBeenCalledWith('save_memory');
|
||||
expect(mockSaveMemoryTool.buildAndExecute).toHaveBeenCalledWith(
|
||||
{ fact },
|
||||
expect.any(AbortSignal),
|
||||
undefined,
|
||||
{
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(mockRefreshMemory).toHaveBeenCalledWith(mockContext.config);
|
||||
expect(response.name).toBe('memory add');
|
||||
expect(response.data).toBe(`Added memory: "${fact}"`);
|
||||
});
|
||||
|
||||
it('returns an error if the tool is not found', async () => {
|
||||
const command = new AddMemoryCommand();
|
||||
const fact = 'another fact';
|
||||
mockAddMemory.mockReturnValue({
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact },
|
||||
});
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
|
||||
|
||||
const response = await command.execute(mockContext, ['another', 'fact']);
|
||||
|
||||
expect(response.name).toBe('memory add');
|
||||
expect(response.data).toBe('Error: Tool save_memory not found.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
addMemory,
|
||||
listMemoryFiles,
|
||||
refreshMemory,
|
||||
showMemory,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Command,
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
|
||||
const DEFAULT_SANITIZATION_CONFIG = {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
};
|
||||
|
||||
export class MemoryCommand implements Command {
|
||||
readonly name = 'memory';
|
||||
readonly description = 'Manage memory.';
|
||||
readonly subCommands = [
|
||||
new ShowMemoryCommand(),
|
||||
new RefreshMemoryCommand(),
|
||||
new ListMemoryCommand(),
|
||||
new AddMemoryCommand(),
|
||||
];
|
||||
readonly topLevel = true;
|
||||
readonly requiresWorkspace = true;
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
return new ShowMemoryCommand().execute(context, _);
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowMemoryCommand implements Command {
|
||||
readonly name = 'memory show';
|
||||
readonly description = 'Shows the current memory contents.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = showMemory(context.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
|
||||
export class RefreshMemoryCommand implements Command {
|
||||
readonly name = 'memory refresh';
|
||||
readonly description = 'Refreshes the memory from the source.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = await refreshMemory(context.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
|
||||
export class ListMemoryCommand implements Command {
|
||||
readonly name = 'memory list';
|
||||
readonly description = 'Lists the paths of the GEMINI.md files in use.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = listMemoryFiles(context.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
|
||||
export class AddMemoryCommand implements Command {
|
||||
readonly name = 'memory add';
|
||||
readonly description = 'Add content to the memory.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const textToAdd = args.join(' ').trim();
|
||||
const result = addMemory(textToAdd);
|
||||
if (result.type === 'message') {
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
|
||||
const toolRegistry = context.config.getToolRegistry();
|
||||
const tool = toolRegistry.getTool(result.toolName);
|
||||
if (tool) {
|
||||
const abortController = new AbortController();
|
||||
const signal = abortController.signal;
|
||||
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
|
||||
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
|
||||
});
|
||||
await refreshMemory(context.config);
|
||||
return {
|
||||
name: this.name,
|
||||
data: `Added memory: "${textToAdd}"`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: this.name,
|
||||
data: `Error: Tool ${result.toolName} not found.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
import type { TelemetryTarget } from '@google/gemini-cli-core';
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
type ExtensionLoader,
|
||||
startupProfiler,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
homedir,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { logger } from '../utils/logger.js';
|
||||
@@ -37,6 +37,10 @@ export async function loadConfig(
|
||||
const workspaceDir = process.cwd();
|
||||
const adcFilePath = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
|
||||
|
||||
const folderTrust =
|
||||
settings.folderTrust === true ||
|
||||
process.env['GEMINI_FOLDER_TRUST'] === 'true';
|
||||
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: taskId,
|
||||
model: settings.general?.previewFeatures
|
||||
@@ -72,26 +76,30 @@ export async function loadConfig(
|
||||
settings.fileFiltering?.enableRecursiveFileSearch,
|
||||
},
|
||||
ideMode: false,
|
||||
folderTrust: settings.folderTrust === true,
|
||||
folderTrust,
|
||||
trustedFolder: true,
|
||||
extensionLoader,
|
||||
checkpointing: process.env['CHECKPOINTING']
|
||||
? process.env['CHECKPOINTING'] === 'true'
|
||||
: settings.checkpointing?.enabled,
|
||||
previewFeatures: settings.general?.previewFeatures,
|
||||
interactive: true,
|
||||
enableInteractiveShell: true,
|
||||
};
|
||||
|
||||
const fileService = new FileDiscoveryService(workspaceDir);
|
||||
const { memoryContent, fileCount } = await loadServerHierarchicalMemory(
|
||||
workspaceDir,
|
||||
[workspaceDir],
|
||||
false,
|
||||
fileService,
|
||||
extensionLoader,
|
||||
settings.folderTrust === true,
|
||||
);
|
||||
const { memoryContent, fileCount, filePaths } =
|
||||
await loadServerHierarchicalMemory(
|
||||
workspaceDir,
|
||||
[workspaceDir],
|
||||
false,
|
||||
fileService,
|
||||
extensionLoader,
|
||||
folderTrust,
|
||||
);
|
||||
configParams.userMemory = memoryContent;
|
||||
configParams.geminiMdFileCount = fileCount;
|
||||
configParams.geminiMdFilePaths = filePaths;
|
||||
const config = new Config({
|
||||
...configParams,
|
||||
});
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
type MCPServerConfig,
|
||||
type ExtensionInstallMetadata,
|
||||
type GeminiCLIExtension,
|
||||
homedir,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
|
||||
@@ -39,7 +39,7 @@ interface ExtensionConfig {
|
||||
export function loadExtensions(workspaceDir: string): GeminiCLIExtension[] {
|
||||
const allExtensions = [
|
||||
...loadExtensionsFromDir(workspaceDir),
|
||||
...loadExtensionsFromDir(os.homedir()),
|
||||
...loadExtensionsFromDir(homedir()),
|
||||
];
|
||||
|
||||
const uniqueExtensions: GeminiCLIExtension[] = [];
|
||||
|
||||
@@ -27,13 +27,21 @@ vi.mock('node:os', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
GEMINI_DIR: '.gemini',
|
||||
debugLogger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
getErrorMessage: (error: unknown) => String(error),
|
||||
}));
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
const path = await import('node:path');
|
||||
const os = await import('node:os');
|
||||
return {
|
||||
...actual,
|
||||
GEMINI_DIR: '.gemini',
|
||||
debugLogger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
getErrorMessage: (error: unknown) => String(error),
|
||||
homedir: () => path.join(os.tmpdir(), `gemini-home-${mocks.suffix}`),
|
||||
};
|
||||
});
|
||||
|
||||
describe('loadSettings', () => {
|
||||
const mockHomeDir = path.join(os.tmpdir(), `gemini-home-${mocks.suffix}`);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import {
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
GEMINI_DIR,
|
||||
getErrorMessage,
|
||||
type TelemetrySettings,
|
||||
homedir,
|
||||
} from '@google/gemini-cli-core';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { gzipSync, gunzipSync } from 'node:zlib';
|
||||
import * as tar from 'tar';
|
||||
import * as fse from 'fs-extra';
|
||||
import { promises as fsPromises, createReadStream } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { tmpdir } from '@google/gemini-cli-core';
|
||||
import { join } from 'node:path';
|
||||
import type { Task as SDKTask } from '@a2a-js/sdk';
|
||||
import type { TaskStore } from '@a2a-js/sdk/server';
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
GeminiClient,
|
||||
HookSystem,
|
||||
PolicyDecision,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
|
||||
import type { Config, Storage } from '@google/gemini-cli-core';
|
||||
@@ -77,6 +78,17 @@ export function createMockConfig(
|
||||
mockConfig.getGeminiClient = vi
|
||||
.fn()
|
||||
.mockReturnValue(new GeminiClient(mockConfig));
|
||||
|
||||
mockConfig.getPolicyEngine = vi.fn().mockReturnValue({
|
||||
check: async () => {
|
||||
const mode = mockConfig.getApprovalMode();
|
||||
if (mode === ApprovalMode.YOLO) {
|
||||
return { decision: PolicyDecision.ALLOW };
|
||||
}
|
||||
return { decision: PolicyDecision.ASK_USER };
|
||||
},
|
||||
});
|
||||
|
||||
return mockConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.24.0-nightly.20251227.37be16243",
|
||||
"version": "0.25.2",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.24.0-nightly.20251227.37be16243"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.25.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.11.0",
|
||||
@@ -45,7 +45,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.4.6",
|
||||
"ink": "npm:@jrichman/ink@6.4.7",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
|
||||
@@ -14,7 +14,7 @@ import { enableCommand } from './extensions/enable.js';
|
||||
import { linkCommand } from './extensions/link.js';
|
||||
import { newCommand } from './extensions/new.js';
|
||||
import { validateCommand } from './extensions/validate.js';
|
||||
import { settingsCommand } from './extensions/settings.js';
|
||||
import { configureCommand } from './extensions/configure.js';
|
||||
import { initializeOutputListenersAndFlush } from '../gemini.js';
|
||||
|
||||
export const extensionsCommand: CommandModule = {
|
||||
@@ -33,7 +33,7 @@ export const extensionsCommand: CommandModule = {
|
||||
.command(linkCommand)
|
||||
.command(newCommand)
|
||||
.command(validateCommand)
|
||||
.command(settingsCommand)
|
||||
.command(configureCommand)
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
handler: () => {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { configureCommand } from './configure.js';
|
||||
import yargs from 'yargs';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import {
|
||||
updateSetting,
|
||||
promptForSetting,
|
||||
getScopedEnvContents,
|
||||
type ExtensionSetting,
|
||||
} from '../../config/extensions/extensionSettings.js';
|
||||
import prompts from 'prompts';
|
||||
|
||||
const {
|
||||
mockExtensionManager,
|
||||
mockGetExtensionAndManager,
|
||||
mockGetExtensionManager,
|
||||
mockLoadSettings,
|
||||
} = vi.hoisted(() => {
|
||||
const extensionManager = {
|
||||
loadExtensionConfig: vi.fn(),
|
||||
getExtensions: vi.fn(),
|
||||
loadExtensions: vi.fn(),
|
||||
getSettings: vi.fn(),
|
||||
};
|
||||
return {
|
||||
mockExtensionManager: extensionManager,
|
||||
mockGetExtensionAndManager: vi.fn(),
|
||||
mockGetExtensionManager: vi.fn(),
|
||||
mockLoadSettings: vi.fn().mockReturnValue({ merged: {} }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/extension-manager.js', () => ({
|
||||
ExtensionManager: vi.fn().mockImplementation(() => mockExtensionManager),
|
||||
}));
|
||||
|
||||
vi.mock('../../config/extensions/extensionSettings.js', () => ({
|
||||
updateSetting: vi.fn(),
|
||||
promptForSetting: vi.fn(),
|
||||
getScopedEnvContents: vi.fn(),
|
||||
ExtensionSettingScope: {
|
||||
USER: 'user',
|
||||
WORKSPACE: 'workspace',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./utils.js', () => ({
|
||||
getExtensionAndManager: mockGetExtensionAndManager,
|
||||
getExtensionManager: mockGetExtensionManager,
|
||||
}));
|
||||
|
||||
vi.mock('prompts');
|
||||
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
}));
|
||||
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
loadSettings: mockLoadSettings,
|
||||
}));
|
||||
|
||||
describe('extensions configure command', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(debugLogger, 'log');
|
||||
vi.spyOn(debugLogger, 'error');
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Default behaviors
|
||||
mockLoadSettings.mockReturnValue({ merged: {} });
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
extension: null,
|
||||
extensionManager: null,
|
||||
});
|
||||
mockGetExtensionManager.mockResolvedValue(mockExtensionManager);
|
||||
(ExtensionManager as unknown as Mock).mockImplementation(
|
||||
() => mockExtensionManager,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const runCommand = async (command: string) => {
|
||||
const parser = yargs().command(configureCommand).help(false).version(false);
|
||||
await parser.parse(command);
|
||||
};
|
||||
|
||||
const setupExtension = (
|
||||
name: string,
|
||||
settings: Array<Partial<ExtensionSetting>> = [],
|
||||
id = 'test-id',
|
||||
path = '/test/path',
|
||||
) => {
|
||||
const extension = { name, path, id };
|
||||
mockGetExtensionAndManager.mockImplementation(async (n) => {
|
||||
if (n === name)
|
||||
return { extension, extensionManager: mockExtensionManager };
|
||||
return { extension: null, extensionManager: null };
|
||||
});
|
||||
|
||||
mockExtensionManager.getExtensions.mockReturnValue([extension]);
|
||||
mockExtensionManager.loadExtensionConfig.mockResolvedValue({
|
||||
name,
|
||||
settings,
|
||||
});
|
||||
return extension;
|
||||
};
|
||||
|
||||
describe('Specific setting configuration', () => {
|
||||
it('should configure a specific setting', async () => {
|
||||
setupExtension('test-ext', [
|
||||
{ name: 'Test Setting', envVar: 'TEST_VAR' },
|
||||
]);
|
||||
(updateSetting as Mock).mockResolvedValue(undefined);
|
||||
|
||||
await runCommand('config test-ext TEST_VAR');
|
||||
|
||||
expect(updateSetting).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'test-ext' }),
|
||||
'test-id',
|
||||
'TEST_VAR',
|
||||
promptForSetting,
|
||||
'user',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing extension', async () => {
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
extension: null,
|
||||
extensionManager: null,
|
||||
});
|
||||
|
||||
await runCommand('config missing-ext TEST_VAR');
|
||||
|
||||
expect(updateSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject invalid extension names', async () => {
|
||||
await runCommand('config ../invalid TEST_VAR');
|
||||
expect(debugLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid extension name'),
|
||||
);
|
||||
|
||||
await runCommand('config ext/with/slash TEST_VAR');
|
||||
expect(debugLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid extension name'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Extension configuration (all settings)', () => {
|
||||
it('should configure all settings for an extension', async () => {
|
||||
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
|
||||
setupExtension('test-ext', settings);
|
||||
(getScopedEnvContents as Mock).mockResolvedValue({});
|
||||
(updateSetting as Mock).mockResolvedValue(undefined);
|
||||
|
||||
await runCommand('config test-ext');
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
'Configuring settings for "test-ext"...',
|
||||
);
|
||||
expect(updateSetting).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'test-ext' }),
|
||||
'test-id',
|
||||
'VAR_1',
|
||||
promptForSetting,
|
||||
'user',
|
||||
);
|
||||
});
|
||||
|
||||
it('should verify overwrite if setting is already set', async () => {
|
||||
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
|
||||
setupExtension('test-ext', settings);
|
||||
(getScopedEnvContents as Mock).mockImplementation(
|
||||
async (_config, _id, scope) => {
|
||||
if (scope === 'user') return { VAR_1: 'existing' };
|
||||
return {};
|
||||
},
|
||||
);
|
||||
(prompts as unknown as Mock).mockResolvedValue({ overwrite: true });
|
||||
(updateSetting as Mock).mockResolvedValue(undefined);
|
||||
|
||||
await runCommand('config test-ext');
|
||||
|
||||
expect(prompts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'confirm',
|
||||
message: expect.stringContaining('is already set. Overwrite?'),
|
||||
}),
|
||||
);
|
||||
expect(updateSetting).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should note if setting is configured in workspace', async () => {
|
||||
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
|
||||
setupExtension('test-ext', settings);
|
||||
(getScopedEnvContents as Mock).mockImplementation(
|
||||
async (_config, _id, scope) => {
|
||||
if (scope === 'workspace') return { VAR_1: 'workspace_value' };
|
||||
return {};
|
||||
},
|
||||
);
|
||||
(updateSetting as Mock).mockResolvedValue(undefined);
|
||||
|
||||
await runCommand('config test-ext');
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('is already configured in the workspace scope'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip update if user denies overwrite', async () => {
|
||||
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
|
||||
setupExtension('test-ext', settings);
|
||||
(getScopedEnvContents as Mock).mockResolvedValue({ VAR_1: 'existing' });
|
||||
(prompts as unknown as Mock).mockResolvedValue({ overwrite: false });
|
||||
|
||||
await runCommand('config test-ext');
|
||||
|
||||
expect(prompts).toHaveBeenCalled();
|
||||
expect(updateSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configure all extensions', () => {
|
||||
it('should configure settings for all installed extensions', async () => {
|
||||
const ext1 = {
|
||||
name: 'ext1',
|
||||
path: '/p1',
|
||||
id: 'id1',
|
||||
settings: [{ envVar: 'V1' }],
|
||||
};
|
||||
const ext2 = {
|
||||
name: 'ext2',
|
||||
path: '/p2',
|
||||
id: 'id2',
|
||||
settings: [{ envVar: 'V2' }],
|
||||
};
|
||||
mockExtensionManager.getExtensions.mockReturnValue([ext1, ext2]);
|
||||
|
||||
mockExtensionManager.loadExtensionConfig.mockImplementation(
|
||||
async (path) => {
|
||||
if (path === '/p1')
|
||||
return { name: 'ext1', settings: [{ name: 'S1', envVar: 'V1' }] };
|
||||
if (path === '/p2')
|
||||
return { name: 'ext2', settings: [{ name: 'S2', envVar: 'V2' }] };
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
(getScopedEnvContents as Mock).mockResolvedValue({});
|
||||
(updateSetting as Mock).mockResolvedValue(undefined);
|
||||
|
||||
await runCommand('config');
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Configuring settings for "ext1"'),
|
||||
);
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Configuring settings for "ext2"'),
|
||||
);
|
||||
expect(updateSetting).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should log if no extensions installed', async () => {
|
||||
mockExtensionManager.getExtensions.mockReturnValue([]);
|
||||
await runCommand('config');
|
||||
expect(debugLogger.log).toHaveBeenCalledWith('No extensions installed.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
updateSetting,
|
||||
promptForSetting,
|
||||
ExtensionSettingScope,
|
||||
getScopedEnvContents,
|
||||
} from '../../config/extensions/extensionSettings.js';
|
||||
import { getExtensionAndManager, getExtensionManager } from './utils.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { debugLogger, coreEvents } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import prompts from 'prompts';
|
||||
import type { ExtensionConfig } from '../../config/extension.js';
|
||||
interface ConfigureArgs {
|
||||
name?: string;
|
||||
setting?: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export const configureCommand: CommandModule<object, ConfigureArgs> = {
|
||||
command: 'config [name] [setting]',
|
||||
describe: 'Configure extension settings.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'Name of the extension to configure.',
|
||||
type: 'string',
|
||||
})
|
||||
.positional('setting', {
|
||||
describe: 'The specific setting to configure (name or env var).',
|
||||
type: 'string',
|
||||
})
|
||||
.option('scope', {
|
||||
describe: 'The scope to set the setting in.',
|
||||
type: 'string',
|
||||
choices: ['user', 'workspace'],
|
||||
default: 'user',
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const { name, setting, scope } = args;
|
||||
const settings = loadSettings(process.cwd()).merged;
|
||||
|
||||
if (!(settings.experimental?.extensionConfig ?? true)) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'Extension configuration is currently disabled. Enable it by setting "experimental.extensionConfig" to true.',
|
||||
);
|
||||
await exitCli();
|
||||
return;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
if (name.includes('/') || name.includes('\\') || name.includes('..')) {
|
||||
debugLogger.error(
|
||||
'Invalid extension name. Names cannot contain path separators or "..".',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 1: Configure specific setting for an extension
|
||||
if (name && setting) {
|
||||
await configureSpecificSetting(
|
||||
name,
|
||||
setting,
|
||||
scope as ExtensionSettingScope,
|
||||
);
|
||||
}
|
||||
// Case 2: Configure all settings for an extension
|
||||
else if (name) {
|
||||
await configureExtension(name, scope as ExtensionSettingScope);
|
||||
}
|
||||
// Case 3: Configure all extensions
|
||||
else {
|
||||
await configureAllExtensions(scope as ExtensionSettingScope);
|
||||
}
|
||||
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
async function configureSpecificSetting(
|
||||
extensionName: string,
|
||||
settingKey: string,
|
||||
scope: ExtensionSettingScope,
|
||||
) {
|
||||
const { extension, extensionManager } =
|
||||
await getExtensionAndManager(extensionName);
|
||||
if (!extension || !extensionManager) {
|
||||
return;
|
||||
}
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (!extensionConfig) {
|
||||
debugLogger.error(
|
||||
`Could not find configuration for extension "${extensionName}".`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateSetting(
|
||||
extensionConfig,
|
||||
extension.id,
|
||||
settingKey,
|
||||
promptForSetting,
|
||||
scope,
|
||||
);
|
||||
}
|
||||
|
||||
async function configureExtension(
|
||||
extensionName: string,
|
||||
scope: ExtensionSettingScope,
|
||||
) {
|
||||
const { extension, extensionManager } =
|
||||
await getExtensionAndManager(extensionName);
|
||||
if (!extension || !extensionManager) {
|
||||
return;
|
||||
}
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (
|
||||
!extensionConfig ||
|
||||
!extensionConfig.settings ||
|
||||
extensionConfig.settings.length === 0
|
||||
) {
|
||||
debugLogger.log(
|
||||
`Extension "${extensionName}" has no settings to configure.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debugLogger.log(`Configuring settings for "${extensionName}"...`);
|
||||
await configureExtensionSettings(extensionConfig, extension.id, scope);
|
||||
}
|
||||
|
||||
async function configureAllExtensions(scope: ExtensionSettingScope) {
|
||||
const extensionManager = await getExtensionManager();
|
||||
const extensions = extensionManager.getExtensions();
|
||||
|
||||
if (extensions.length === 0) {
|
||||
debugLogger.log('No extensions installed.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const extension of extensions) {
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (
|
||||
extensionConfig &&
|
||||
extensionConfig.settings &&
|
||||
extensionConfig.settings.length > 0
|
||||
) {
|
||||
debugLogger.log(`\nConfiguring settings for "${extension.name}"...`);
|
||||
await configureExtensionSettings(extensionConfig, extension.id, scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function configureExtensionSettings(
|
||||
extensionConfig: ExtensionConfig,
|
||||
extensionId: string,
|
||||
scope: ExtensionSettingScope,
|
||||
) {
|
||||
const currentScopedSettings = await getScopedEnvContents(
|
||||
extensionConfig,
|
||||
extensionId,
|
||||
scope,
|
||||
);
|
||||
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
if (scope === ExtensionSettingScope.USER) {
|
||||
workspaceSettings = await getScopedEnvContents(
|
||||
extensionConfig,
|
||||
extensionId,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
);
|
||||
}
|
||||
|
||||
if (!extensionConfig.settings) return;
|
||||
|
||||
for (const setting of extensionConfig.settings) {
|
||||
const currentValue = currentScopedSettings[setting.envVar];
|
||||
const workspaceValue = workspaceSettings[setting.envVar];
|
||||
|
||||
if (workspaceValue !== undefined) {
|
||||
debugLogger.log(
|
||||
`Note: Setting "${setting.name}" is already configured in the workspace scope.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (currentValue !== undefined) {
|
||||
const response = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'overwrite',
|
||||
message: `Setting "${setting.name}" (${setting.envVar}) is already set. Overwrite?`,
|
||||
initial: false,
|
||||
});
|
||||
|
||||
if (!response.overwrite) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await updateSetting(
|
||||
extensionConfig,
|
||||
extensionId,
|
||||
setting.envVar,
|
||||
promptForSetting,
|
||||
scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { settingsCommand } from './settings.js';
|
||||
import yargs from 'yargs';
|
||||
import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import type { getExtensionAndManager } from './utils.js';
|
||||
import type {
|
||||
updateSetting,
|
||||
getScopedEnvContents,
|
||||
} from '../../config/extensions/extensionSettings.js';
|
||||
import {
|
||||
promptForSetting,
|
||||
ExtensionSettingScope,
|
||||
} from '../../config/extensions/extensionSettings.js';
|
||||
import type { exitCli } from '../utils.js';
|
||||
import type { ExtensionManager } from '../../config/extension-manager.js';
|
||||
|
||||
const mockGetExtensionAndManager: Mock<typeof getExtensionAndManager> =
|
||||
vi.hoisted(() => vi.fn());
|
||||
const mockUpdateSetting: Mock<typeof updateSetting> = vi.hoisted(() => vi.fn());
|
||||
const mockGetScopedEnvContents: Mock<typeof getScopedEnvContents> = vi.hoisted(
|
||||
() => vi.fn(),
|
||||
);
|
||||
const mockExitCli: Mock<typeof exitCli> = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./utils.js', () => ({
|
||||
getExtensionAndManager: mockGetExtensionAndManager,
|
||||
}));
|
||||
|
||||
vi.mock('../../config/extensions/extensionSettings.js', () => ({
|
||||
updateSetting: mockUpdateSetting,
|
||||
promptForSetting: vi.fn(),
|
||||
ExtensionSettingScope: {
|
||||
USER: 'user',
|
||||
WORKSPACE: 'workspace',
|
||||
},
|
||||
getScopedEnvContents: mockGetScopedEnvContents,
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
debugLogger: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: mockExitCli,
|
||||
}));
|
||||
|
||||
describe('settings command', () => {
|
||||
let debugLogSpy: Mock;
|
||||
let debugErrorSpy: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
debugLogSpy = debugLogger.log as Mock;
|
||||
debugErrorSpy = debugLogger.error as Mock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('set command', () => {
|
||||
it('should log error and exit if extension is not found', async () => {
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
extension: null,
|
||||
extensionManager: null,
|
||||
});
|
||||
|
||||
await yargs([])
|
||||
.command(settingsCommand)
|
||||
.parseAsync('settings set foo bar');
|
||||
|
||||
expect(mockExitCli).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log error and exit if extension config is not found', async () => {
|
||||
const mockExtensionManager = {
|
||||
loadExtensionConfig: vi.fn().mockResolvedValue(null),
|
||||
} as unknown as ExtensionManager;
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
extension: { path: '/path/to/ext' } as unknown as GeminiCLIExtension,
|
||||
extensionManager: mockExtensionManager,
|
||||
});
|
||||
|
||||
await yargs([])
|
||||
.command(settingsCommand)
|
||||
.parseAsync('settings set foo bar');
|
||||
|
||||
expect(debugErrorSpy).toHaveBeenCalledWith(
|
||||
'Could not find configuration for extension "foo".',
|
||||
);
|
||||
expect(mockExitCli).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call updateSetting with correct arguments', async () => {
|
||||
const mockExtensionManager = {
|
||||
loadExtensionConfig: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as ExtensionManager;
|
||||
const extension = { path: '/path/to/ext', id: 'ext-id' };
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
extension: extension as unknown as GeminiCLIExtension,
|
||||
extensionManager: mockExtensionManager,
|
||||
});
|
||||
|
||||
await yargs([])
|
||||
.command(settingsCommand)
|
||||
.parseAsync('settings set foo bar --scope workspace');
|
||||
|
||||
expect(mockUpdateSetting).toHaveBeenCalledWith(
|
||||
{},
|
||||
'ext-id',
|
||||
'bar',
|
||||
promptForSetting,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
);
|
||||
expect(mockExitCli).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('list command', () => {
|
||||
it('should log error and exit if extension is not found', async () => {
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
extension: null,
|
||||
extensionManager: null,
|
||||
});
|
||||
|
||||
await yargs([]).command(settingsCommand).parseAsync('settings list foo');
|
||||
|
||||
expect(mockExitCli).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log message and exit if extension has no settings', async () => {
|
||||
const mockExtensionManager = {
|
||||
loadExtensionConfig: vi.fn().mockResolvedValue({ settings: [] }),
|
||||
} as unknown as ExtensionManager;
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
extension: { path: '/path/to/ext' } as unknown as GeminiCLIExtension,
|
||||
extensionManager: mockExtensionManager,
|
||||
});
|
||||
|
||||
await yargs([]).command(settingsCommand).parseAsync('settings list foo');
|
||||
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(
|
||||
'Extension "foo" has no settings to configure.',
|
||||
);
|
||||
expect(mockExitCli).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should list settings correctly', async () => {
|
||||
const mockExtensionManager = {
|
||||
loadExtensionConfig: vi.fn().mockResolvedValue({
|
||||
settings: [
|
||||
{
|
||||
name: 'Setting 1',
|
||||
envVar: 'SETTING_1',
|
||||
description: 'Desc 1',
|
||||
sensitive: false,
|
||||
},
|
||||
{
|
||||
name: 'Setting 2',
|
||||
envVar: 'SETTING_2',
|
||||
description: 'Desc 2',
|
||||
sensitive: true,
|
||||
},
|
||||
{
|
||||
name: 'Setting 3',
|
||||
envVar: 'SETTING_3',
|
||||
description: 'Desc 3',
|
||||
sensitive: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as unknown as ExtensionManager;
|
||||
const extension = { path: '/path/to/ext', id: 'ext-id' };
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
extension: extension as unknown as GeminiCLIExtension,
|
||||
extensionManager: mockExtensionManager,
|
||||
});
|
||||
|
||||
mockGetScopedEnvContents.mockImplementation((_config, _id, scope) => {
|
||||
if (scope === ExtensionSettingScope.USER) {
|
||||
return Promise.resolve({
|
||||
SETTING_1: 'val1',
|
||||
SETTING_2: 'val2',
|
||||
});
|
||||
}
|
||||
if (scope === ExtensionSettingScope.WORKSPACE) {
|
||||
return Promise.resolve({
|
||||
SETTING_3: 'val3',
|
||||
});
|
||||
}
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
await yargs([]).command(settingsCommand).parseAsync('settings list foo');
|
||||
|
||||
expect(debugLogSpy).toHaveBeenCalledWith('Settings for "foo":');
|
||||
// Setting 1 (User)
|
||||
expect(debugLogSpy).toHaveBeenCalledWith('\n- Setting 1 (SETTING_1)');
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(' Description: Desc 1');
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(' Value: val1 (user)');
|
||||
// Setting 2 (Sensitive)
|
||||
expect(debugLogSpy).toHaveBeenCalledWith('\n- Setting 2 (SETTING_2)');
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(' Description: Desc 2');
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(
|
||||
' Value: [value stored in keychain] (user)',
|
||||
);
|
||||
// Setting 3 (Workspace)
|
||||
expect(debugLogSpy).toHaveBeenCalledWith('\n- Setting 3 (SETTING_3)');
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(' Description: Desc 3');
|
||||
expect(debugLogSpy).toHaveBeenCalledWith(' Value: val3 (workspace)');
|
||||
|
||||
expect(mockExitCli).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
updateSetting,
|
||||
promptForSetting,
|
||||
ExtensionSettingScope,
|
||||
getScopedEnvContents,
|
||||
} from '../../config/extensions/extensionSettings.js';
|
||||
import { getExtensionAndManager } from './utils.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
// --- SET COMMAND ---
|
||||
interface SetArgs {
|
||||
name: string;
|
||||
setting: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
const setCommand: CommandModule<object, SetArgs> = {
|
||||
command: 'set [--scope] <name> <setting>',
|
||||
describe: 'Set a specific setting for an extension.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'Name of the extension to configure.',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.positional('setting', {
|
||||
describe: 'The setting to configure (name or env var).',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('scope', {
|
||||
describe: 'The scope to set the setting in.',
|
||||
type: 'string',
|
||||
choices: ['user', 'workspace'],
|
||||
default: 'user',
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const { name, setting, scope } = args;
|
||||
const { extension, extensionManager } = await getExtensionAndManager(name);
|
||||
if (!extension || !extensionManager) {
|
||||
await exitCli();
|
||||
return;
|
||||
}
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (!extensionConfig) {
|
||||
debugLogger.error(
|
||||
`Could not find configuration for extension "${name}".`,
|
||||
);
|
||||
await exitCli();
|
||||
return;
|
||||
}
|
||||
await updateSetting(
|
||||
extensionConfig,
|
||||
extension.id,
|
||||
setting,
|
||||
promptForSetting,
|
||||
scope as ExtensionSettingScope,
|
||||
);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
// --- LIST COMMAND ---
|
||||
interface ListArgs {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const listCommand: CommandModule<object, ListArgs> = {
|
||||
command: 'list <name>',
|
||||
describe: 'List all settings for an extension.',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('name', {
|
||||
describe: 'Name of the extension.',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const { name } = args;
|
||||
const { extension, extensionManager } = await getExtensionAndManager(name);
|
||||
if (!extension || !extensionManager) {
|
||||
await exitCli();
|
||||
return;
|
||||
}
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (
|
||||
!extensionConfig ||
|
||||
!extensionConfig.settings ||
|
||||
extensionConfig.settings.length === 0
|
||||
) {
|
||||
debugLogger.log(`Extension "${name}" has no settings to configure.`);
|
||||
await exitCli();
|
||||
return;
|
||||
}
|
||||
|
||||
const userSettings = await getScopedEnvContents(
|
||||
extensionConfig,
|
||||
extension.id,
|
||||
ExtensionSettingScope.USER,
|
||||
);
|
||||
const workspaceSettings = await getScopedEnvContents(
|
||||
extensionConfig,
|
||||
extension.id,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
);
|
||||
const mergedSettings = { ...userSettings, ...workspaceSettings };
|
||||
|
||||
debugLogger.log(`Settings for "${name}":`);
|
||||
for (const setting of extensionConfig.settings) {
|
||||
const value = mergedSettings[setting.envVar];
|
||||
let displayValue: string;
|
||||
let scopeInfo = '';
|
||||
|
||||
if (workspaceSettings[setting.envVar] !== undefined) {
|
||||
scopeInfo = ' (workspace)';
|
||||
} else if (userSettings[setting.envVar] !== undefined) {
|
||||
scopeInfo = ' (user)';
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
displayValue = '[not set]';
|
||||
} else if (setting.sensitive) {
|
||||
displayValue = '[value stored in keychain]';
|
||||
} else {
|
||||
displayValue = value;
|
||||
}
|
||||
debugLogger.log(`
|
||||
- ${setting.name} (${setting.envVar})`);
|
||||
debugLogger.log(` Description: ${setting.description}`);
|
||||
debugLogger.log(` Value: ${displayValue}${scopeInfo}`);
|
||||
}
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
// --- SETTINGS COMMAND ---
|
||||
export const settingsCommand: CommandModule = {
|
||||
command: 'settings <command>',
|
||||
describe: 'Manage extension settings.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(setCommand)
|
||||
.command(listCommand)
|
||||
.demandCommand(1, 'You need to specify a command (set or list).')
|
||||
.version(false),
|
||||
handler: () => {
|
||||
// This handler is not called when a subcommand is provided.
|
||||
// Yargs will show the help menu.
|
||||
},
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import { loadSettings } from '../../config/settings.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
export async function getExtensionAndManager(name: string) {
|
||||
export async function getExtensionManager() {
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
@@ -19,6 +19,11 @@ export async function getExtensionAndManager(name: string) {
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
return extensionManager;
|
||||
}
|
||||
|
||||
export async function getExtensionAndManager(name: string) {
|
||||
const extensionManager = await getExtensionManager();
|
||||
const extension = extensionManager
|
||||
.getExtensions()
|
||||
.find((ext) => ext.name === name);
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { CommandModule } from 'yargs';
|
||||
import { listCommand } from './skills/list.js';
|
||||
import { enableCommand } from './skills/enable.js';
|
||||
import { disableCommand } from './skills/disable.js';
|
||||
import { installCommand } from './skills/install.js';
|
||||
import { uninstallCommand } from './skills/uninstall.js';
|
||||
import { initializeOutputListenersAndFlush } from '../gemini.js';
|
||||
|
||||
export const skillsCommand: CommandModule = {
|
||||
@@ -20,6 +22,8 @@ export const skillsCommand: CommandModule = {
|
||||
.command(listCommand)
|
||||
.command(enableCommand)
|
||||
.command(disableCommand)
|
||||
.command(installCommand)
|
||||
.command(uninstallCommand)
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
handler: () => {
|
||||
|
||||
@@ -36,6 +36,7 @@ vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
loadSettings: vi.fn(),
|
||||
isLoadableSettingScope: vi.fn((s) => s === 'User' || s === 'Workspace'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -59,6 +60,7 @@ describe('skills disable command', () => {
|
||||
const mockSettings = {
|
||||
forScope: vi.fn().mockReturnValue({
|
||||
settings: { skills: { disabled: [] } },
|
||||
path: '/user/settings.json',
|
||||
}),
|
||||
setValue: vi.fn(),
|
||||
};
|
||||
@@ -78,7 +80,7 @@ describe('skills disable command', () => {
|
||||
);
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
'Skill "skill1" successfully disabled in scope "User".',
|
||||
'Skill "skill1" disabled by adding it to the disabled list in user (/user/settings.json) settings.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -86,29 +88,27 @@ describe('skills disable command', () => {
|
||||
const mockSettings = {
|
||||
forScope: vi.fn().mockReturnValue({
|
||||
settings: { skills: { disabled: ['skill1'] } },
|
||||
path: '/user/settings.json',
|
||||
}),
|
||||
setValue: vi.fn(),
|
||||
};
|
||||
mockLoadSettings.mockReturnValue(
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
mockSettings as unknown as LoadedSettings,
|
||||
);
|
||||
|
||||
await handleDisable({
|
||||
name: 'skill1',
|
||||
scope: SettingScope.User as LoadableSettingScope,
|
||||
});
|
||||
await handleDisable({ name: 'skill1', scope: SettingScope.User });
|
||||
|
||||
expect(mockSettings.setValue).not.toHaveBeenCalled();
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
'Skill "skill1" is already disabled in scope "User".',
|
||||
'Skill "skill1" is already disabled.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableCommand', () => {
|
||||
it('should have correct command and describe', () => {
|
||||
expect(disableCommand.command).toBe('disable <name>');
|
||||
expect(disableCommand.command).toBe('disable <name> [--scope]');
|
||||
expect(disableCommand.describe).toBe('Disables an agent skill.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,17 +5,16 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
loadSettings,
|
||||
SettingScope,
|
||||
type LoadableSettingScope,
|
||||
} from '../../config/settings.js';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { disableSkill } from '../../utils/skillSettings.js';
|
||||
import { renderSkillActionFeedback } from '../../utils/skillUtils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface DisableArgs {
|
||||
name: string;
|
||||
scope: LoadableSettingScope;
|
||||
scope: SettingScope;
|
||||
}
|
||||
|
||||
export async function handleDisable(args: DisableArgs) {
|
||||
@@ -23,21 +22,16 @@ export async function handleDisable(args: DisableArgs) {
|
||||
const workspaceDir = process.cwd();
|
||||
const settings = loadSettings(workspaceDir);
|
||||
|
||||
const currentDisabled =
|
||||
settings.forScope(scope).settings.skills?.disabled || [];
|
||||
|
||||
if (currentDisabled.includes(name)) {
|
||||
debugLogger.log(`Skill "${name}" is already disabled in scope "${scope}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newDisabled = [...currentDisabled, name];
|
||||
settings.setValue(scope, 'skills.disabled', newDisabled);
|
||||
debugLogger.log(`Skill "${name}" successfully disabled in scope "${scope}".`);
|
||||
const result = disableSkill(settings, name, scope);
|
||||
const feedback = renderSkillActionFeedback(
|
||||
result,
|
||||
(label, path) => `${chalk.bold(label)} (${chalk.dim(path)})`,
|
||||
);
|
||||
debugLogger.log(feedback);
|
||||
}
|
||||
|
||||
export const disableCommand: CommandModule = {
|
||||
command: 'disable <name>',
|
||||
command: 'disable <name> [--scope]',
|
||||
describe: 'Disables an agent skill.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
@@ -50,11 +44,11 @@ export const disableCommand: CommandModule = {
|
||||
alias: 's',
|
||||
describe: 'The scope to disable the skill in (user or project).',
|
||||
type: 'string',
|
||||
default: 'user',
|
||||
default: 'project',
|
||||
choices: ['user', 'project'],
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const scope: LoadableSettingScope =
|
||||
const scope =
|
||||
argv['scope'] === 'project' ? SettingScope.Workspace : SettingScope.User;
|
||||
await handleDisable({
|
||||
name: argv['name'] as string,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
loadSettings,
|
||||
SettingScope,
|
||||
type LoadedSettings,
|
||||
type LoadableSettingScope,
|
||||
} from '../../config/settings.js';
|
||||
|
||||
const emitConsoleLog = vi.hoisted(() => vi.fn());
|
||||
@@ -36,6 +35,7 @@ vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
loadSettings: vi.fn(),
|
||||
isLoadableSettingScope: vi.fn((s) => s === 'User' || s === 'Workspace'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -57,8 +57,14 @@ describe('skills enable command', () => {
|
||||
describe('handleEnable', () => {
|
||||
it('should enable a disabled skill in user scope', async () => {
|
||||
const mockSettings = {
|
||||
forScope: vi.fn().mockReturnValue({
|
||||
settings: { skills: { disabled: ['skill1'] } },
|
||||
forScope: vi.fn().mockImplementation((scope) => {
|
||||
if (scope === SettingScope.User) {
|
||||
return {
|
||||
settings: { skills: { disabled: ['skill1'] } },
|
||||
path: '/user/settings.json',
|
||||
};
|
||||
}
|
||||
return { settings: {}, path: '/project/settings.json' };
|
||||
}),
|
||||
setValue: vi.fn(),
|
||||
};
|
||||
@@ -66,10 +72,7 @@ describe('skills enable command', () => {
|
||||
mockSettings as unknown as LoadedSettings,
|
||||
);
|
||||
|
||||
await handleEnable({
|
||||
name: 'skill1',
|
||||
scope: SettingScope.User as LoadableSettingScope,
|
||||
});
|
||||
await handleEnable({ name: 'skill1' });
|
||||
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
@@ -78,14 +81,26 @@ describe('skills enable command', () => {
|
||||
);
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
'Skill "skill1" successfully enabled in scope "User".',
|
||||
'Skill "skill1" enabled by removing it from the disabled list in user (/user/settings.json) and project (/project/settings.json) settings.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should log a message if the skill is already enabled', async () => {
|
||||
it('should enable a skill across multiple scopes', async () => {
|
||||
const mockSettings = {
|
||||
forScope: vi.fn().mockReturnValue({
|
||||
settings: { skills: { disabled: [] } },
|
||||
forScope: vi.fn().mockImplementation((scope) => {
|
||||
if (scope === SettingScope.User) {
|
||||
return {
|
||||
settings: { skills: { disabled: ['skill1'] } },
|
||||
path: '/user/settings.json',
|
||||
};
|
||||
}
|
||||
if (scope === SettingScope.Workspace) {
|
||||
return {
|
||||
settings: { skills: { disabled: ['skill1'] } },
|
||||
path: '/workspace/settings.json',
|
||||
};
|
||||
}
|
||||
return { settings: {}, path: '' };
|
||||
}),
|
||||
setValue: vi.fn(),
|
||||
};
|
||||
@@ -93,15 +108,42 @@ describe('skills enable command', () => {
|
||||
mockSettings as unknown as LoadedSettings,
|
||||
);
|
||||
|
||||
await handleEnable({
|
||||
name: 'skill1',
|
||||
scope: SettingScope.User as LoadableSettingScope,
|
||||
});
|
||||
await handleEnable({ name: 'skill1' });
|
||||
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'skills.disabled',
|
||||
[],
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.Workspace,
|
||||
'skills.disabled',
|
||||
[],
|
||||
);
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
'Skill "skill1" enabled by removing it from the disabled list in project (/workspace/settings.json) and user (/user/settings.json) settings.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should log a message if the skill is already enabled', async () => {
|
||||
const mockSettings = {
|
||||
forScope: vi.fn().mockReturnValue({
|
||||
settings: { skills: { disabled: [] } },
|
||||
path: '/user/settings.json',
|
||||
}),
|
||||
setValue: vi.fn(),
|
||||
};
|
||||
mockLoadSettings.mockReturnValue(
|
||||
mockSettings as unknown as LoadedSettings,
|
||||
);
|
||||
|
||||
await handleEnable({ name: 'skill1' });
|
||||
|
||||
expect(mockSettings.setValue).not.toHaveBeenCalled();
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
'Skill "skill1" is already enabled in scope "User".',
|
||||
'Skill "skill1" is already enabled.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,60 +5,42 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
loadSettings,
|
||||
SettingScope,
|
||||
type LoadableSettingScope,
|
||||
} from '../../config/settings.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { enableSkill } from '../../utils/skillSettings.js';
|
||||
import { renderSkillActionFeedback } from '../../utils/skillUtils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface EnableArgs {
|
||||
name: string;
|
||||
scope: LoadableSettingScope;
|
||||
}
|
||||
|
||||
export async function handleEnable(args: EnableArgs) {
|
||||
const { name, scope } = args;
|
||||
const { name } = args;
|
||||
const workspaceDir = process.cwd();
|
||||
const settings = loadSettings(workspaceDir);
|
||||
|
||||
const currentDisabled =
|
||||
settings.forScope(scope).settings.skills?.disabled || [];
|
||||
const newDisabled = currentDisabled.filter((d) => d !== name);
|
||||
|
||||
if (currentDisabled.length === newDisabled.length) {
|
||||
debugLogger.log(`Skill "${name}" is already enabled in scope "${scope}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
settings.setValue(scope, 'skills.disabled', newDisabled);
|
||||
debugLogger.log(`Skill "${name}" successfully enabled in scope "${scope}".`);
|
||||
const result = enableSkill(settings, name);
|
||||
const feedback = renderSkillActionFeedback(
|
||||
result,
|
||||
(label, path) => `${chalk.bold(label)} (${chalk.dim(path)})`,
|
||||
);
|
||||
debugLogger.log(feedback);
|
||||
}
|
||||
|
||||
export const enableCommand: CommandModule = {
|
||||
command: 'enable <name>',
|
||||
describe: 'Enables an agent skill.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'The name of the skill to enable.',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('scope', {
|
||||
alias: 's',
|
||||
describe: 'The scope to enable the skill in (user or project).',
|
||||
type: 'string',
|
||||
default: 'user',
|
||||
choices: ['user', 'project'],
|
||||
}),
|
||||
yargs.positional('name', {
|
||||
describe: 'The name of the skill to enable.',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const scope: LoadableSettingScope =
|
||||
argv['scope'] === 'project' ? SettingScope.Workspace : SettingScope.User;
|
||||
await handleEnable({
|
||||
name: argv['name'] as string,
|
||||
scope,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const mockInstallSkill = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../utils/skillUtils.js', () => ({
|
||||
installSkill: mockInstallSkill,
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
debugLogger: { log: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { handleInstall, installCommand } from './install.js';
|
||||
|
||||
describe('skill install command', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
||||
});
|
||||
|
||||
describe('installCommand', () => {
|
||||
it('should have correct command and describe', () => {
|
||||
expect(installCommand.command).toBe(
|
||||
'install <source> [--scope] [--path]',
|
||||
);
|
||||
expect(installCommand.describe).toBe(
|
||||
'Installs an agent skill from a git repository URL or a local path.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call installSkill with correct arguments for user scope', async () => {
|
||||
mockInstallSkill.mockResolvedValue([
|
||||
{ name: 'test-skill', location: '/mock/user/skills/test-skill' },
|
||||
]);
|
||||
|
||||
await handleInstall({
|
||||
source: 'https://example.com/repo.git',
|
||||
scope: 'user',
|
||||
});
|
||||
|
||||
expect(mockInstallSkill).toHaveBeenCalledWith(
|
||||
'https://example.com/repo.git',
|
||||
'user',
|
||||
undefined,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Successfully installed skill: test-skill'),
|
||||
);
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('location: /mock/user/skills/test-skill'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call installSkill with correct arguments for workspace scope and subpath', async () => {
|
||||
mockInstallSkill.mockResolvedValue([
|
||||
{ name: 'test-skill', location: '/mock/workspace/skills/test-skill' },
|
||||
]);
|
||||
|
||||
await handleInstall({
|
||||
source: 'https://example.com/repo.git',
|
||||
scope: 'workspace',
|
||||
path: 'my-skills-dir',
|
||||
});
|
||||
|
||||
expect(mockInstallSkill).toHaveBeenCalledWith(
|
||||
'https://example.com/repo.git',
|
||||
'workspace',
|
||||
'my-skills-dir',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
mockInstallSkill.mockRejectedValue(new Error('Install failed'));
|
||||
|
||||
await handleInstall({ source: '/local/path' });
|
||||
|
||||
expect(debugLogger.error).toHaveBeenCalledWith('Install failed');
|
||||
expect(process.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { installSkill } from '../../utils/skillUtils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface InstallArgs {
|
||||
source: string;
|
||||
scope?: 'user' | 'workspace';
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
try {
|
||||
const { source } = args;
|
||||
const scope = args.scope ?? 'user';
|
||||
const subpath = args.path;
|
||||
|
||||
const installedSkills = await installSkill(
|
||||
source,
|
||||
scope,
|
||||
subpath,
|
||||
(msg) => {
|
||||
debugLogger.log(msg);
|
||||
},
|
||||
);
|
||||
|
||||
for (const skill of installedSkills) {
|
||||
debugLogger.log(
|
||||
chalk.green(
|
||||
`Successfully installed skill: ${chalk.bold(skill.name)} (scope: ${scope}, location: ${skill.location})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
await exitCli(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const installCommand: CommandModule = {
|
||||
command: 'install <source> [--scope] [--path]',
|
||||
describe:
|
||||
'Installs an agent skill from a git repository URL or a local path.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional('source', {
|
||||
describe:
|
||||
'The git repository URL or local path of the skill to install.',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('scope', {
|
||||
describe:
|
||||
'The scope to install the skill into. Defaults to "user" (global).',
|
||||
choices: ['user', 'workspace'],
|
||||
default: 'user',
|
||||
})
|
||||
.option('path', {
|
||||
describe:
|
||||
'Sub-path within the repository to install from (only used for git repository sources).',
|
||||
type: 'string',
|
||||
})
|
||||
.check((argv) => {
|
||||
if (!argv.source) {
|
||||
throw new Error('The source argument must be provided.');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleInstall({
|
||||
source: argv['source'] as string,
|
||||
scope: argv['scope'] as 'user' | 'workspace',
|
||||
path: argv['path'] as string | undefined,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -65,7 +65,7 @@ describe('skills list command', () => {
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await handleList();
|
||||
await handleList({});
|
||||
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -96,7 +96,7 @@ describe('skills list command', () => {
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await handleList();
|
||||
await handleList({});
|
||||
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -120,10 +120,63 @@ describe('skills list command', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter built-in skills by default and show them with { all: true }', async () => {
|
||||
const skills = [
|
||||
{
|
||||
name: 'regular',
|
||||
description: 'desc1',
|
||||
disabled: false,
|
||||
location: '/loc1',
|
||||
},
|
||||
{
|
||||
name: 'builtin',
|
||||
description: 'desc2',
|
||||
disabled: false,
|
||||
location: '/loc2',
|
||||
isBuiltin: true,
|
||||
},
|
||||
];
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getSkillManager: vi.fn().mockReturnValue({
|
||||
getAllSkills: vi.fn().mockReturnValue(skills),
|
||||
}),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
// Default
|
||||
await handleList({ all: false });
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining('regular'),
|
||||
);
|
||||
expect(emitConsoleLog).not.toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining('builtin'),
|
||||
);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// With all: true
|
||||
await handleList({ all: true });
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining('regular'),
|
||||
);
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining('builtin'),
|
||||
);
|
||||
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining(chalk.gray(' [Built-in]')),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when listing fails', async () => {
|
||||
mockLoadCliConfig.mockRejectedValue(new Error('List failed'));
|
||||
|
||||
await expect(handleList()).rejects.toThrow('List failed');
|
||||
await expect(handleList({})).rejects.toThrow('List failed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,7 +184,7 @@ describe('skills list command', () => {
|
||||
const command = listCommand;
|
||||
|
||||
it('should have correct command and describe', () => {
|
||||
expect(command.command).toBe('list');
|
||||
expect(command.command).toBe('list [--all]');
|
||||
expect(command.describe).toBe('Lists discovered agent skills.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function handleList() {
|
||||
export async function handleList(args: { all?: boolean }) {
|
||||
const workspaceDir = process.cwd();
|
||||
const settings = loadSettings(workspaceDir);
|
||||
|
||||
@@ -28,7 +28,17 @@ export async function handleList() {
|
||||
await config.initialize();
|
||||
|
||||
const skillManager = config.getSkillManager();
|
||||
const skills = skillManager.getAllSkills();
|
||||
const skills = args.all
|
||||
? skillManager.getAllSkills()
|
||||
: skillManager.getAllSkills().filter((s) => !s.isBuiltin);
|
||||
|
||||
// Sort skills: non-built-in first, then alphabetically by name
|
||||
skills.sort((a, b) => {
|
||||
if (a.isBuiltin === b.isBuiltin) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
return a.isBuiltin ? 1 : -1;
|
||||
});
|
||||
|
||||
if (skills.length === 0) {
|
||||
debugLogger.log('No skills discovered.');
|
||||
@@ -43,7 +53,9 @@ export async function handleList() {
|
||||
? chalk.red('[Disabled]')
|
||||
: chalk.green('[Enabled]');
|
||||
|
||||
debugLogger.log(`${chalk.bold(skill.name)} ${status}`);
|
||||
const builtinSuffix = skill.isBuiltin ? chalk.gray(' [Built-in]') : '';
|
||||
|
||||
debugLogger.log(`${chalk.bold(skill.name)} ${status}${builtinSuffix}`);
|
||||
debugLogger.log(` Description: ${skill.description}`);
|
||||
debugLogger.log(` Location: ${skill.location}`);
|
||||
debugLogger.log('');
|
||||
@@ -51,11 +63,16 @@ export async function handleList() {
|
||||
}
|
||||
|
||||
export const listCommand: CommandModule = {
|
||||
command: 'list',
|
||||
command: 'list [--all]',
|
||||
describe: 'Lists discovered agent skills.',
|
||||
builder: (yargs) => yargs,
|
||||
handler: async () => {
|
||||
await handleList();
|
||||
builder: (yargs) =>
|
||||
yargs.option('all', {
|
||||
type: 'boolean',
|
||||
description: 'Show all skills, including built-in ones.',
|
||||
default: false,
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleList({ all: argv['all'] as boolean });
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const mockUninstallSkill = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../utils/skillUtils.js', () => ({
|
||||
uninstallSkill: mockUninstallSkill,
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
debugLogger: { log: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { handleUninstall, uninstallCommand } from './uninstall.js';
|
||||
|
||||
describe('skill uninstall command', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
||||
});
|
||||
|
||||
describe('uninstallCommand', () => {
|
||||
it('should have correct command and describe', () => {
|
||||
expect(uninstallCommand.command).toBe('uninstall <name> [--scope]');
|
||||
expect(uninstallCommand.describe).toBe(
|
||||
'Uninstalls an agent skill by name.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call uninstallSkill with correct arguments for user scope', async () => {
|
||||
mockUninstallSkill.mockResolvedValue({
|
||||
location: '/mock/user/skills/test-skill',
|
||||
});
|
||||
|
||||
await handleUninstall({
|
||||
name: 'test-skill',
|
||||
scope: 'user',
|
||||
});
|
||||
|
||||
expect(mockUninstallSkill).toHaveBeenCalledWith('test-skill', 'user');
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Successfully uninstalled skill: test-skill'),
|
||||
);
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('location: /mock/user/skills/test-skill'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call uninstallSkill with correct arguments for workspace scope', async () => {
|
||||
mockUninstallSkill.mockResolvedValue({
|
||||
location: '/mock/workspace/skills/test-skill',
|
||||
});
|
||||
|
||||
await handleUninstall({
|
||||
name: 'test-skill',
|
||||
scope: 'workspace',
|
||||
});
|
||||
|
||||
expect(mockUninstallSkill).toHaveBeenCalledWith('test-skill', 'workspace');
|
||||
});
|
||||
|
||||
it('should log an error if skill is not found', async () => {
|
||||
mockUninstallSkill.mockResolvedValue(null);
|
||||
|
||||
await handleUninstall({ name: 'test-skill' });
|
||||
|
||||
expect(debugLogger.error).toHaveBeenCalledWith(
|
||||
'Skill "test-skill" is not installed in the user scope.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
mockUninstallSkill.mockRejectedValue(new Error('Uninstall failed'));
|
||||
|
||||
await handleUninstall({ name: 'test-skill' });
|
||||
|
||||
expect(debugLogger.error).toHaveBeenCalledWith('Uninstall failed');
|
||||
expect(process.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { uninstallSkill } from '../../utils/skillUtils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface UninstallArgs {
|
||||
name: string;
|
||||
scope?: 'user' | 'workspace';
|
||||
}
|
||||
|
||||
export async function handleUninstall(args: UninstallArgs) {
|
||||
try {
|
||||
const { name } = args;
|
||||
const scope = args.scope ?? 'user';
|
||||
|
||||
const result = await uninstallSkill(name, scope);
|
||||
|
||||
if (result) {
|
||||
debugLogger.log(
|
||||
chalk.green(
|
||||
`Successfully uninstalled skill: ${chalk.bold(name)} (scope: ${scope}, location: ${result.location})`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
debugLogger.error(
|
||||
`Skill "${name}" is not installed in the ${scope} scope.`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
await exitCli(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const uninstallCommand: CommandModule = {
|
||||
command: 'uninstall <name> [--scope]',
|
||||
describe: 'Uninstalls an agent skill by name.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'The name of the skill to uninstall.',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('scope', {
|
||||
describe:
|
||||
'The scope to uninstall the skill from. Defaults to "user" (global).',
|
||||
choices: ['user', 'workspace'],
|
||||
default: 'user',
|
||||
})
|
||||
.check((argv) => {
|
||||
if (!argv.name) {
|
||||
throw new Error('The skill name must be provided.');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleUninstall({
|
||||
name: argv['name'] as string,
|
||||
scope: argv['scope'] as 'user' | 'workspace',
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -2413,6 +2413,45 @@ describe('Policy Engine Integration in loadCliConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig disableYoloMode', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should allow auto_edit mode even if yolo mode is disabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode=auto_edit'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
security: { disableYoloMode: true },
|
||||
};
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
|
||||
});
|
||||
|
||||
it('should throw if YOLO mode is attempted when disableYoloMode is true', async () => {
|
||||
process.argv = ['node', 'script.js', '--yolo'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
security: { disableYoloMode: true },
|
||||
};
|
||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||
'Cannot start in YOLO mode since it is disabled by your admin',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig secureModeEnabled', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
@@ -53,7 +53,7 @@ import { requestConsentNonInteractive } from './extensions/consent.js';
|
||||
import { promptForSetting } from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
import { runExitCleanup } from '../utils/cleanup.js';
|
||||
import { getEnableHooks } from './settingsSchema.js';
|
||||
import { getEnableHooks, getEnableHooksUI } from './settingsSchema.js';
|
||||
|
||||
export interface CliArgs {
|
||||
query: string | undefined;
|
||||
@@ -292,7 +292,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
}
|
||||
|
||||
// Register hooks command if hooks are enabled
|
||||
if (getEnableHooks(settings)) {
|
||||
if (getEnableHooksUI(settings)) {
|
||||
yargsInstance.command(hooksCommand);
|
||||
}
|
||||
|
||||
@@ -520,7 +520,6 @@ export async function loadCliConfig(
|
||||
'Cannot start in YOLO mode since it is disabled by your admin',
|
||||
);
|
||||
}
|
||||
approvalMode = ApprovalMode.DEFAULT;
|
||||
} else if (approvalMode === ApprovalMode.YOLO) {
|
||||
debugLogger.warn(
|
||||
'YOLO mode is enabled. All tool calls will be automatically approved.',
|
||||
@@ -637,6 +636,8 @@ export async function loadCliConfig(
|
||||
const ptyInfo = await getPty();
|
||||
|
||||
const mcpEnabled = settings.admin?.mcp?.enabled ?? true;
|
||||
const extensionsEnabled = settings.admin?.extensions?.enabled ?? true;
|
||||
const adminSkillsEnabled = settings.admin?.skills?.enabled ?? true;
|
||||
|
||||
return new Config({
|
||||
sessionId,
|
||||
@@ -659,6 +660,9 @@ export async function loadCliConfig(
|
||||
mcpServerCommand: mcpEnabled ? settings.mcp?.serverCommand : undefined,
|
||||
mcpServers: mcpEnabled ? settings.mcpServers : {},
|
||||
mcpEnabled,
|
||||
extensionsEnabled,
|
||||
agents: settings.agents,
|
||||
adminSkillsEnabled,
|
||||
allowedMcpServers: mcpEnabled
|
||||
? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
|
||||
: undefined,
|
||||
@@ -706,6 +710,7 @@ export async function loadCliConfig(
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
skillsSupport: settings.experimental?.skills,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
@@ -731,15 +736,16 @@ export async function loadCliConfig(
|
||||
},
|
||||
codebaseInvestigatorSettings:
|
||||
settings.experimental?.codebaseInvestigatorSettings,
|
||||
introspectionAgentSettings:
|
||||
settings.experimental?.introspectionAgentSettings,
|
||||
cliHelpAgentSettings: settings.experimental?.cliHelpAgentSettings,
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors,
|
||||
ptyInfo: ptyInfo?.name,
|
||||
disableLLMCorrection: settings.tools?.disableLLMCorrection,
|
||||
modelConfigServiceConfig: settings.modelConfigs,
|
||||
// TODO: loading of hooks based on workspace trust
|
||||
enableHooks: getEnableHooks(settings),
|
||||
enableHooksUI: getEnableHooksUI(settings),
|
||||
hooks: settings.hooks || {},
|
||||
projectHooks: projectHooks || {},
|
||||
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
|
||||
@@ -747,6 +753,8 @@ export async function loadCliConfig(
|
||||
const refreshedSettings = loadSettings(cwd);
|
||||
return {
|
||||
disabledSkills: refreshedSettings.merged.skills?.disabled,
|
||||
adminSkillsEnabled:
|
||||
refreshedSettings.merged.admin?.skills?.enabled ?? adminSkillsEnabled,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { type Settings } from './settings.js';
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
|
||||
|
||||
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
||||
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
return {
|
||||
...actual,
|
||||
homedir: mockHomedir,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock @google/gemini-cli-core
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const core = await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...core,
|
||||
homedir: mockHomedir,
|
||||
loadAgentsFromDirectory: core.loadAgentsFromDirectory,
|
||||
loadSkillsFromDir: core.loadSkillsFromDir,
|
||||
};
|
||||
});
|
||||
|
||||
describe('ExtensionManager agents loading', () => {
|
||||
let extensionManager: ExtensionManager;
|
||||
let tempDir: string;
|
||||
let extensionsDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
|
||||
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-test-agents-'));
|
||||
mockHomedir.mockReturnValue(tempDir);
|
||||
|
||||
// Create the extensions directory that ExtensionManager expects
|
||||
extensionsDir = path.join(tempDir, '.gemini', EXTENSIONS_DIRECTORY_NAME);
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
settings: {
|
||||
telemetry: { enabled: false },
|
||||
trustedFolders: [tempDir],
|
||||
} as unknown as Settings,
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: vi.fn(),
|
||||
workspaceDir: tempDir,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
it('should load agents from an extension', async () => {
|
||||
const sourceDir = path.join(tempDir, 'source-ext-good');
|
||||
createExtension({
|
||||
extensionsDir: sourceDir,
|
||||
name: 'good-agents-ext',
|
||||
version: '1.0.0',
|
||||
installMetadata: {
|
||||
type: 'local',
|
||||
source: path.join(sourceDir, 'good-agents-ext'),
|
||||
},
|
||||
});
|
||||
const extensionPath = path.join(sourceDir, 'good-agents-ext');
|
||||
|
||||
const agentsDir = path.join(extensionPath, 'agents');
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(agentsDir, 'test-agent.md'),
|
||||
'---\nname: test-agent\nkind: local\ndescription: test desc\n---\nbody',
|
||||
);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
const extension = await extensionManager.installOrUpdateExtension({
|
||||
type: 'local',
|
||||
source: extensionPath,
|
||||
});
|
||||
|
||||
expect(extension.name).toBe('good-agents-ext');
|
||||
expect(extension.agents).toBeDefined();
|
||||
expect(extension.agents).toHaveLength(1);
|
||||
expect(extension.agents![0].name).toBe('test-agent');
|
||||
expect(debugLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log errors but continue if an agent fails to load', async () => {
|
||||
const sourceDir = path.join(tempDir, 'source-ext-bad');
|
||||
createExtension({
|
||||
extensionsDir: sourceDir,
|
||||
name: 'bad-agents-ext',
|
||||
version: '1.0.0',
|
||||
installMetadata: {
|
||||
type: 'local',
|
||||
source: path.join(sourceDir, 'bad-agents-ext'),
|
||||
},
|
||||
});
|
||||
const extensionPath = path.join(sourceDir, 'bad-agents-ext');
|
||||
|
||||
const agentsDir = path.join(extensionPath, 'agents');
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
// Invalid agent (missing description)
|
||||
fs.writeFileSync(
|
||||
path.join(agentsDir, 'bad-agent.md'),
|
||||
'---\nname: bad-agent\nkind: local\n---\nbody',
|
||||
);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
const extension = await extensionManager.installOrUpdateExtension({
|
||||
type: 'local',
|
||||
source: extensionPath,
|
||||
});
|
||||
|
||||
expect(extension.name).toBe('bad-agents-ext');
|
||||
expect(extension.agents).toEqual([]);
|
||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Error loading agent from bad-agents-ext'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import type { Settings } from './settings.js';
|
||||
import {
|
||||
loadAgentsFromDirectory,
|
||||
loadSkillsFromDir,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
let currentTempHome = '';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
homedir: () => currentTempHome,
|
||||
debugLogger: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
loadAgentsFromDirectory: vi.fn().mockImplementation(async () => ({
|
||||
agents: [],
|
||||
errors: [],
|
||||
})),
|
||||
loadSkillsFromDir: vi.fn().mockImplementation(async () => []),
|
||||
};
|
||||
});
|
||||
|
||||
describe('ExtensionManager Settings Scope', () => {
|
||||
const extensionName = 'test-extension';
|
||||
let tempWorkspace: string;
|
||||
let extensionsDir: string;
|
||||
let extensionDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.mocked(loadAgentsFromDirectory).mockResolvedValue({
|
||||
agents: [],
|
||||
errors: [],
|
||||
});
|
||||
vi.mocked(loadSkillsFromDir).mockResolvedValue([]);
|
||||
currentTempHome = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||
);
|
||||
tempWorkspace = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-test-workspace-'),
|
||||
);
|
||||
extensionsDir = path.join(currentTempHome, '.gemini', 'extensions');
|
||||
extensionDir = path.join(extensionsDir, extensionName);
|
||||
|
||||
fs.mkdirSync(extensionDir, { recursive: true });
|
||||
|
||||
// Create gemini-extension.json
|
||||
const extensionConfig = {
|
||||
name: extensionName,
|
||||
version: '1.0.0',
|
||||
settings: [
|
||||
{
|
||||
name: 'Test Setting',
|
||||
envVar: 'TEST_SETTING',
|
||||
description: 'A test setting',
|
||||
},
|
||||
],
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(extensionDir, 'gemini-extension.json'),
|
||||
JSON.stringify(extensionConfig),
|
||||
);
|
||||
|
||||
// Create install metadata
|
||||
const installMetadata = {
|
||||
source: extensionDir,
|
||||
type: 'local',
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(extensionDir, 'install-metadata.json'),
|
||||
JSON.stringify(installMetadata),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up files if needed, or rely on temp dir cleanup
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should prioritize workspace settings over user settings and report correct scope', async () => {
|
||||
// 1. Set User Setting
|
||||
const userSettingsPath = path.join(extensionDir, '.env');
|
||||
fs.writeFileSync(userSettingsPath, 'TEST_SETTING=user-value');
|
||||
|
||||
// 2. Set Workspace Setting
|
||||
const workspaceSettingsPath = path.join(tempWorkspace, '.env');
|
||||
fs.writeFileSync(workspaceSettingsPath, 'TEST_SETTING=workspace-value');
|
||||
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspace,
|
||||
requestConsent: async () => true,
|
||||
requestSetting: async () => '',
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === extensionName);
|
||||
|
||||
expect(extension).toBeDefined();
|
||||
|
||||
// Verify resolved settings
|
||||
const setting = extension?.resolvedSettings?.find(
|
||||
(s) => s.envVar === 'TEST_SETTING',
|
||||
);
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting?.value).toBe('workspace-value');
|
||||
expect(setting?.scope).toBe('workspace');
|
||||
expect(setting?.source).toBe(workspaceSettingsPath);
|
||||
|
||||
// Verify output string contains (Workspace - <path>)
|
||||
const output = extensionManager.toOutputString(extension!);
|
||||
expect(output).toContain(
|
||||
`Test Setting: workspace-value (Workspace - ${workspaceSettingsPath})`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to user settings if workspace setting is missing', async () => {
|
||||
// 1. Set User Setting
|
||||
const userSettingsPath = path.join(extensionDir, '.env');
|
||||
fs.writeFileSync(userSettingsPath, 'TEST_SETTING=user-value');
|
||||
|
||||
// 2. No Workspace Setting
|
||||
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspace,
|
||||
requestConsent: async () => true,
|
||||
requestSetting: async () => '',
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === extensionName);
|
||||
|
||||
expect(extension).toBeDefined();
|
||||
|
||||
// Verify resolved settings
|
||||
const setting = extension?.resolvedSettings?.find(
|
||||
(s) => s.envVar === 'TEST_SETTING',
|
||||
);
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting?.value).toBe('user-value');
|
||||
expect(setting?.scope).toBe('user');
|
||||
expect(setting?.source?.endsWith(path.join(extensionName, '.env'))).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
// Verify output string contains (User - <path>)
|
||||
const output = extensionManager.toOutputString(extension!);
|
||||
expect(output).toContain(
|
||||
`Test Setting: user-value (User - ${userSettingsPath})`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should report unset if neither is present', async () => {
|
||||
// No settings files
|
||||
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspace,
|
||||
requestConsent: async () => true,
|
||||
requestSetting: async () => '',
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const extension = extensions.find((e) => e.name === extensionName);
|
||||
|
||||
expect(extension).toBeDefined();
|
||||
|
||||
// Verify resolved settings
|
||||
const setting = extension?.resolvedSettings?.find(
|
||||
(s) => s.envVar === 'TEST_SETTING',
|
||||
);
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting?.value).toBe('[not set]');
|
||||
expect(setting?.scope).toBeUndefined();
|
||||
|
||||
// Verify output string does not contain scope
|
||||
const output = extensionManager.toOutputString(extension!);
|
||||
expect(output).toContain('Test Setting: [not set]');
|
||||
expect(output).not.toContain('Test Setting: [not set] (User)');
|
||||
expect(output).not.toContain('Test Setting: [not set] (Workspace)');
|
||||
});
|
||||
});
|
||||
@@ -4,115 +4,167 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { loadSettings } from './settings.js';
|
||||
import { debugLogger, coreEvents } from '@google/gemini-cli-core';
|
||||
import { type Settings } from './settings.js';
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
|
||||
const mockHomedir = vi.hoisted(() => vi.fn());
|
||||
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const mockedOs = await importOriginal<typeof import('node:os')>();
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
return {
|
||||
...mockedOs,
|
||||
...actual,
|
||||
homedir: mockHomedir,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock @google/gemini-cli-core
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
homedir: mockHomedir,
|
||||
loadAgentsFromDirectory: vi
|
||||
.fn()
|
||||
.mockImplementation(async () => ({ agents: [], errors: [] })),
|
||||
loadSkillsFromDir: (
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>()
|
||||
).loadSkillsFromDir,
|
||||
};
|
||||
});
|
||||
|
||||
describe('ExtensionManager skills validation', () => {
|
||||
let tempHomeDir: string;
|
||||
let tempWorkspaceDir: string;
|
||||
let userExtensionsDir: string;
|
||||
let extensionManager: ExtensionManager;
|
||||
let tempDir: string;
|
||||
let extensionsDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHomeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-skills-test-home-'),
|
||||
);
|
||||
tempWorkspaceDir = fs.mkdtempSync(
|
||||
path.join(tempHomeDir, 'gemini-cli-skills-test-workspace-'),
|
||||
);
|
||||
userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
|
||||
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(coreEvents, 'emitFeedback');
|
||||
vi.spyOn(debugLogger, 'debug').mockImplementation(() => {});
|
||||
|
||||
mockHomedir.mockReturnValue(tempHomeDir);
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-test-'));
|
||||
mockHomedir.mockReturnValue(tempDir);
|
||||
|
||||
// Create the extensions directory that ExtensionManager expects
|
||||
extensionsDir = path.join(tempDir, '.gemini', EXTENSIONS_DIRECTORY_NAME);
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
settings: {
|
||||
telemetry: { enabled: false },
|
||||
trustedFolders: [tempDir],
|
||||
} as unknown as Settings,
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: vi.fn().mockResolvedValue(''),
|
||||
settings: loadSettings(tempWorkspaceDir).merged,
|
||||
requestSetting: vi.fn(),
|
||||
workspaceDir: tempDir,
|
||||
});
|
||||
vi.spyOn(coreEvents, 'emitFeedback');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
vi.restoreAllMocks();
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
it('should emit a warning during install if skills directory is not empty but no skills are loaded', async () => {
|
||||
const sourceExtDir = createExtension({
|
||||
extensionsDir: tempHomeDir,
|
||||
// Create a source extension
|
||||
const sourceDir = path.join(tempDir, 'source-ext');
|
||||
createExtension({
|
||||
extensionsDir: sourceDir, // createExtension appends name
|
||||
name: 'skills-ext',
|
||||
version: '1.0.0',
|
||||
installMetadata: {
|
||||
type: 'local',
|
||||
source: path.join(sourceDir, 'skills-ext'),
|
||||
},
|
||||
});
|
||||
const extensionPath = path.join(sourceDir, 'skills-ext');
|
||||
|
||||
const skillsDir = path.join(sourceExtDir, 'skills');
|
||||
// Add invalid skills content
|
||||
const skillsDir = path.join(extensionPath, 'skills');
|
||||
fs.mkdirSync(skillsDir);
|
||||
fs.writeFileSync(path.join(skillsDir, 'not-a-skill.txt'), 'hello');
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
const extension = await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
type: 'local',
|
||||
source: extensionPath,
|
||||
});
|
||||
|
||||
expect(extension.name).toBe('skills-ext');
|
||||
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
expect(debugLogger.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to load skills from'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit a warning during load if skills directory is not empty but no skills are loaded', async () => {
|
||||
const extDir = createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'load-skills-ext',
|
||||
// 1. Create a source extension
|
||||
const sourceDir = path.join(tempDir, 'source-ext-load');
|
||||
createExtension({
|
||||
extensionsDir: sourceDir,
|
||||
name: 'skills-ext-load',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const sourceExtPath = path.join(sourceDir, 'skills-ext-load');
|
||||
|
||||
const skillsDir = path.join(extDir, 'skills');
|
||||
// Add invalid skills content
|
||||
const skillsDir = path.join(sourceExtPath, 'skills');
|
||||
fs.mkdirSync(skillsDir);
|
||||
fs.writeFileSync(path.join(skillsDir, 'not-a-skill.txt'), 'hello');
|
||||
|
||||
// 2. Install it to ensure correct disk state
|
||||
await extensionManager.loadExtensions();
|
||||
await extensionManager.installOrUpdateExtension({
|
||||
type: 'local',
|
||||
source: sourceExtPath,
|
||||
});
|
||||
|
||||
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
// Clear the spy
|
||||
vi.mocked(debugLogger.debug).mockClear();
|
||||
|
||||
// 3. Create a fresh ExtensionManager to force loading from disk
|
||||
const newExtensionManager = new ExtensionManager({
|
||||
settings: {
|
||||
telemetry: { enabled: false },
|
||||
trustedFolders: [tempDir],
|
||||
} as unknown as Settings,
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: vi.fn(),
|
||||
workspaceDir: tempDir,
|
||||
});
|
||||
|
||||
// 4. Load extensions
|
||||
await newExtensionManager.loadExtensions();
|
||||
|
||||
expect(debugLogger.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to load skills from'),
|
||||
);
|
||||
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
expect.stringContaining(
|
||||
'The directory is not empty but no valid skills were discovered',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should succeed if skills are correctly loaded', async () => {
|
||||
const sourceExtDir = createExtension({
|
||||
extensionsDir: tempHomeDir,
|
||||
const sourceDir = path.join(tempDir, 'source-ext-good');
|
||||
createExtension({
|
||||
extensionsDir: sourceDir,
|
||||
name: 'good-skills-ext',
|
||||
version: '1.0.0',
|
||||
installMetadata: {
|
||||
type: 'local',
|
||||
source: path.join(sourceDir, 'good-skills-ext'),
|
||||
},
|
||||
});
|
||||
const extensionPath = path.join(sourceDir, 'good-skills-ext');
|
||||
|
||||
const skillsDir = path.join(sourceExtDir, 'skills');
|
||||
const skillsDir = path.join(extensionPath, 'skills');
|
||||
const skillSubdir = path.join(skillsDir, 'test-skill');
|
||||
fs.mkdirSync(skillSubdir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -121,17 +173,14 @@ describe('ExtensionManager skills validation', () => {
|
||||
);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
const extension = await extensionManager.installOrUpdateExtension({
|
||||
source: sourceExtDir,
|
||||
type: 'local',
|
||||
source: extensionPath,
|
||||
});
|
||||
|
||||
expect(extension.skills).toHaveLength(1);
|
||||
expect(extension.skills![0].name).toBe('test-skill');
|
||||
// It might be called for other reasons during startup, but shouldn't be called for our skills loading success
|
||||
// Actually, it shouldn't be called with our warning message
|
||||
expect(coreEvents.emitFeedback).not.toHaveBeenCalledWith(
|
||||
'warning',
|
||||
expect(extension.name).toBe('good-skills-ext');
|
||||
expect(debugLogger.debug).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to load skills from'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
@@ -39,6 +38,8 @@ import {
|
||||
logExtensionUninstall,
|
||||
logExtensionUpdateEvent,
|
||||
loadSkillsFromDir,
|
||||
loadAgentsFromDirectory,
|
||||
homedir,
|
||||
type ExtensionEvents,
|
||||
type MCPServerConfig,
|
||||
type ExtensionInstallMetadata,
|
||||
@@ -59,9 +60,12 @@ import {
|
||||
} from './extensions/variables.js';
|
||||
import {
|
||||
getEnvContents,
|
||||
getEnvFilePath,
|
||||
maybePromptForSettings,
|
||||
getMissingSettings,
|
||||
type ExtensionSetting,
|
||||
getScopedEnvContents,
|
||||
ExtensionSettingScope,
|
||||
} from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
import { getEnableHooks } from './settingsSchema.js';
|
||||
@@ -277,12 +281,16 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
previousSettings = await getEnvContents(
|
||||
previousExtensionConfig,
|
||||
extensionId,
|
||||
this.workspaceDir,
|
||||
);
|
||||
await this.uninstallExtension(newExtensionName, isUpdate);
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(destinationPath, { recursive: true });
|
||||
if (this.requestSetting) {
|
||||
if (
|
||||
this.requestSetting &&
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
) {
|
||||
if (isUpdate) {
|
||||
await maybePromptForSettings(
|
||||
newExtensionConfig,
|
||||
@@ -300,16 +308,20 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
}
|
||||
|
||||
const missingSettings = await getMissingSettings(
|
||||
newExtensionConfig,
|
||||
extensionId,
|
||||
);
|
||||
const missingSettings =
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
? await getMissingSettings(
|
||||
newExtensionConfig,
|
||||
extensionId,
|
||||
this.workspaceDir,
|
||||
)
|
||||
: [];
|
||||
if (missingSettings.length > 0) {
|
||||
const message = `Extension "${newExtensionConfig.name}" has missing settings: ${missingSettings
|
||||
.map((s) => s.name)
|
||||
.join(
|
||||
', ',
|
||||
)}. Please run "gemini extensions settings ${newExtensionConfig.name} <setting-name>" to configure them.`;
|
||||
)}. Please run "gemini extensions config ${newExtensionConfig.name} [setting-name]" to configure them.`;
|
||||
debugLogger.warn(message);
|
||||
coreEvents.emitFeedback('warning', message);
|
||||
}
|
||||
@@ -465,6 +477,12 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
if (this.loadedExtensions) {
|
||||
throw new Error('Extensions already loaded, only load extensions once.');
|
||||
}
|
||||
|
||||
if (this.settings.admin?.extensions?.enabled === false) {
|
||||
this.loadedExtensions = [];
|
||||
return this.loadedExtensions;
|
||||
}
|
||||
|
||||
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||
this.loadedExtensions = [];
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
@@ -512,16 +530,59 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
);
|
||||
}
|
||||
|
||||
const customEnv = await getEnvContents(
|
||||
config,
|
||||
getExtensionId(config, installMetadata),
|
||||
);
|
||||
const extensionId = getExtensionId(config, installMetadata);
|
||||
|
||||
let userSettings: Record<string, string> = {};
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
|
||||
if (this.settings.experimental?.extensionConfig ?? false) {
|
||||
userSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
ExtensionSettingScope.USER,
|
||||
);
|
||||
workspaceSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
this.workspaceDir,
|
||||
);
|
||||
}
|
||||
|
||||
const customEnv = { ...userSettings, ...workspaceSettings };
|
||||
config = resolveEnvVarsInObject(config, customEnv);
|
||||
|
||||
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
||||
if (config.settings) {
|
||||
if (
|
||||
config.settings &&
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
) {
|
||||
for (const setting of config.settings) {
|
||||
const value = customEnv[setting.envVar];
|
||||
let scope: 'user' | 'workspace' | undefined;
|
||||
let source: string | undefined;
|
||||
|
||||
// Note: strict check for undefined, as empty string is a valid value
|
||||
if (workspaceSettings[setting.envVar] !== undefined) {
|
||||
scope = 'workspace';
|
||||
if (setting.sensitive) {
|
||||
source = 'Keychain';
|
||||
} else {
|
||||
source = getEnvFilePath(
|
||||
config.name,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
this.workspaceDir,
|
||||
);
|
||||
}
|
||||
} else if (userSettings[setting.envVar] !== undefined) {
|
||||
scope = 'user';
|
||||
if (setting.sensitive) {
|
||||
source = 'Keychain';
|
||||
} else {
|
||||
source = getEnvFilePath(config.name, ExtensionSettingScope.USER);
|
||||
}
|
||||
}
|
||||
|
||||
resolvedSettings.push({
|
||||
name: setting.name,
|
||||
envVar: setting.envVar,
|
||||
@@ -532,17 +593,23 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
? '***'
|
||||
: value,
|
||||
sensitive: setting.sensitive ?? false,
|
||||
scope,
|
||||
source,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (config.mcpServers) {
|
||||
config.mcpServers = Object.fromEntries(
|
||||
Object.entries(config.mcpServers).map(([key, value]) => [
|
||||
key,
|
||||
filterMcpConfig(value),
|
||||
]),
|
||||
);
|
||||
if (this.settings.admin?.mcp?.enabled === false) {
|
||||
config.mcpServers = undefined;
|
||||
} else {
|
||||
config.mcpServers = Object.fromEntries(
|
||||
Object.entries(config.mcpServers).map(([key, value]) => [
|
||||
key,
|
||||
filterMcpConfig(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const contextFiles = getContextFileNames(config)
|
||||
@@ -563,6 +630,17 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
path.join(effectiveExtensionPath, 'skills'),
|
||||
);
|
||||
|
||||
const agentLoadResult = await loadAgentsFromDirectory(
|
||||
path.join(effectiveExtensionPath, 'agents'),
|
||||
);
|
||||
|
||||
// Log errors but don't fail the entire extension load
|
||||
for (const error of agentLoadResult.errors) {
|
||||
debugLogger.warn(
|
||||
`[ExtensionManager] Error loading agent from ${config.name}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const extension: GeminiCLIExtension = {
|
||||
name: config.name,
|
||||
version: config.version,
|
||||
@@ -580,6 +658,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
settings: config.settings,
|
||||
resolvedSettings,
|
||||
skills,
|
||||
agents: agentLoadResult.agents,
|
||||
};
|
||||
this.loadedExtensions = [...this.loadedExtensions, extension];
|
||||
|
||||
@@ -692,7 +771,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
toOutputString(extension: GeminiCLIExtension): string {
|
||||
const userEnabled = this.extensionEnablementManager.isEnabled(
|
||||
extension.name,
|
||||
os.homedir(),
|
||||
homedir(),
|
||||
);
|
||||
const workspaceEnabled = this.extensionEnablementManager.isEnabled(
|
||||
extension.name,
|
||||
@@ -744,7 +823,15 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
if (resolvedSettings && resolvedSettings.length > 0) {
|
||||
output += `\n Settings:`;
|
||||
resolvedSettings.forEach((setting) => {
|
||||
output += `\n ${setting.name}: ${setting.value}`;
|
||||
let scope = '';
|
||||
if (setting.scope) {
|
||||
scope = setting.scope === 'workspace' ? '(Workspace' : '(User';
|
||||
if (setting.source) {
|
||||
scope += ` - ${setting.source}`;
|
||||
}
|
||||
scope += ')';
|
||||
}
|
||||
output += `\n ${setting.name}: ${setting.value} ${scope}`;
|
||||
});
|
||||
}
|
||||
return output;
|
||||
@@ -766,7 +853,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
if (scope !== SettingScope.Session) {
|
||||
const scopePath =
|
||||
scope === SettingScope.Workspace ? this.workspaceDir : os.homedir();
|
||||
scope === SettingScope.Workspace ? this.workspaceDir : homedir();
|
||||
this.extensionEnablementManager.disable(name, true, scopePath);
|
||||
}
|
||||
await logExtensionDisable(
|
||||
@@ -801,7 +888,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
if (scope !== SettingScope.Session) {
|
||||
const scopePath =
|
||||
scope === SettingScope.Workspace ? this.workspaceDir : os.homedir();
|
||||
scope === SettingScope.Workspace ? this.workspaceDir : homedir();
|
||||
this.extensionEnablementManager.enable(name, true, scopePath);
|
||||
}
|
||||
await logExtensionEnable(
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
ExtensionDisableEvent,
|
||||
ExtensionEnableEvent,
|
||||
KeychainTokenStorage,
|
||||
loadAgentsFromDirectory,
|
||||
loadSkillsFromDir,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { loadSettings, SettingScope } from './settings.js';
|
||||
import {
|
||||
@@ -105,6 +107,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
logExtensionUninstall: mockLogExtensionUninstall,
|
||||
logExtensionUpdateEvent: mockLogExtensionUpdateEvent,
|
||||
logExtensionDisable: mockLogExtensionDisable,
|
||||
homedir: mockHomedir,
|
||||
ExtensionEnableEvent: vi.fn(),
|
||||
ExtensionInstallEvent: vi.fn(),
|
||||
ExtensionUninstallEvent: vi.fn(),
|
||||
@@ -116,6 +119,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
listSecrets: vi.fn(),
|
||||
isAvailable: vi.fn().mockResolvedValue(true),
|
||||
})),
|
||||
loadAgentsFromDirectory: vi
|
||||
.fn()
|
||||
.mockImplementation(async () => ({ agents: [], errors: [] })),
|
||||
loadSkillsFromDir: vi.fn().mockImplementation(async () => []),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -170,6 +177,11 @@ describe('extension tests', () => {
|
||||
(
|
||||
KeychainTokenStorage as unknown as ReturnType<typeof vi.fn>
|
||||
).mockImplementation(() => mockKeychainStorage);
|
||||
vi.mocked(loadAgentsFromDirectory).mockResolvedValue({
|
||||
agents: [],
|
||||
errors: [],
|
||||
});
|
||||
vi.mocked(loadSkillsFromDir).mockResolvedValue([]);
|
||||
tempHomeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||
);
|
||||
@@ -188,11 +200,13 @@ describe('extension tests', () => {
|
||||
source: undefined,
|
||||
});
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||
const settings = loadSettings(tempWorkspaceDir).merged;
|
||||
(settings.experimental ??= {}).extensionConfig = true;
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: loadSettings(tempWorkspaceDir).merged,
|
||||
settings,
|
||||
});
|
||||
resetTrustedFoldersForTesting();
|
||||
});
|
||||
@@ -631,6 +645,79 @@ describe('extension tests', () => {
|
||||
expect(extension).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not load any extensions if admin.extensions.enabled is false', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const loadedSettings = loadSettings(tempWorkspaceDir).merged;
|
||||
(loadedSettings.admin ??= {}).extensions ??= {};
|
||||
loadedSettings.admin.extensions.enabled = false;
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: loadedSettings,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
expect(extensions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not load mcpServers if admin.mcp.enabled is false', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
mcpServers: {
|
||||
'test-server': { command: 'echo', args: ['hello'] },
|
||||
},
|
||||
});
|
||||
const loadedSettings = loadSettings(tempWorkspaceDir).merged;
|
||||
(loadedSettings.admin ??= {}).mcp ??= {};
|
||||
loadedSettings.admin.mcp.enabled = false;
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: loadedSettings,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].mcpServers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should load mcpServers if admin.mcp.enabled is true', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
mcpServers: {
|
||||
'test-server': { command: 'echo', args: ['hello'] },
|
||||
},
|
||||
});
|
||||
const loadedSettings = loadSettings(tempWorkspaceDir).merged;
|
||||
(loadedSettings.admin ??= {}).mcp ??= {};
|
||||
loadedSettings.admin.mcp.enabled = true;
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: loadedSettings,
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].mcpServers).toEqual({
|
||||
'test-server': { command: 'echo', args: ['hello'] },
|
||||
});
|
||||
});
|
||||
|
||||
describe('id generation', () => {
|
||||
it.each([
|
||||
{
|
||||
|
||||
@@ -529,6 +529,7 @@ describe('extensionSettings', () => {
|
||||
config,
|
||||
extensionId,
|
||||
ExtensionSettingScope.USER,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
expect(contents).toEqual({
|
||||
@@ -552,6 +553,7 @@ describe('extensionSettings', () => {
|
||||
config,
|
||||
extensionId,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
expect(contents).toEqual({
|
||||
@@ -596,7 +598,11 @@ describe('extensionSettings', () => {
|
||||
);
|
||||
await workspaceKeychain.setSecret('VAR2', 'workspace-secret2');
|
||||
|
||||
const contents = await getEnvContents(config, extensionId);
|
||||
const contents = await getEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
expect(contents).toEqual({
|
||||
VAR1: 'workspace-value1',
|
||||
@@ -636,6 +642,7 @@ describe('extensionSettings', () => {
|
||||
'VAR1',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.USER,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
const expectedEnvPath = path.join(extensionDir, '.env');
|
||||
@@ -652,6 +659,7 @@ describe('extensionSettings', () => {
|
||||
'VAR1',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
const expectedEnvPath = path.join(tempWorkspaceDir, '.env');
|
||||
@@ -668,6 +676,7 @@ describe('extensionSettings', () => {
|
||||
'VAR2',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.USER,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
const userKeychain = new KeychainTokenStorage(
|
||||
@@ -685,6 +694,7 @@ describe('extensionSettings', () => {
|
||||
'VAR2',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
const workspaceKeychain = new KeychainTokenStorage(
|
||||
@@ -710,6 +720,7 @@ describe('extensionSettings', () => {
|
||||
'VAR1',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
||||
// Read the .env file after update
|
||||
|
||||
@@ -33,20 +33,28 @@ const getKeychainStorageName = (
|
||||
extensionName: string,
|
||||
extensionId: string,
|
||||
scope: ExtensionSettingScope,
|
||||
workspaceDir?: string,
|
||||
): string => {
|
||||
const base = `Gemini CLI Extensions ${extensionName} ${extensionId}`;
|
||||
if (scope === ExtensionSettingScope.WORKSPACE) {
|
||||
return `${base} ${process.cwd()}`;
|
||||
if (!workspaceDir) {
|
||||
throw new Error('Workspace directory is required for workspace scope');
|
||||
}
|
||||
return `${base} ${workspaceDir}`;
|
||||
}
|
||||
return base;
|
||||
};
|
||||
|
||||
const getEnvFilePath = (
|
||||
export const getEnvFilePath = (
|
||||
extensionName: string,
|
||||
scope: ExtensionSettingScope,
|
||||
workspaceDir?: string,
|
||||
): string => {
|
||||
if (scope === ExtensionSettingScope.WORKSPACE) {
|
||||
return path.join(process.cwd(), EXTENSION_SETTINGS_FILENAME);
|
||||
if (!workspaceDir) {
|
||||
throw new Error('Workspace directory is required for workspace scope');
|
||||
}
|
||||
return path.join(workspaceDir, EXTENSION_SETTINGS_FILENAME);
|
||||
}
|
||||
return new ExtensionStorage(extensionName).getEnvFilePath();
|
||||
};
|
||||
@@ -143,12 +151,13 @@ export async function getScopedEnvContents(
|
||||
extensionConfig: ExtensionConfig,
|
||||
extensionId: string,
|
||||
scope: ExtensionSettingScope,
|
||||
workspaceDir?: string,
|
||||
): Promise<Record<string, string>> {
|
||||
const { name: extensionName } = extensionConfig;
|
||||
const keychain = new KeychainTokenStorage(
|
||||
getKeychainStorageName(extensionName, extensionId, scope),
|
||||
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
|
||||
);
|
||||
const envFilePath = getEnvFilePath(extensionName, scope);
|
||||
const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir);
|
||||
let customEnv: Record<string, string> = {};
|
||||
if (fsSync.existsSync(envFilePath)) {
|
||||
const envFile = fsSync.readFileSync(envFilePath, 'utf-8');
|
||||
@@ -171,6 +180,7 @@ export async function getScopedEnvContents(
|
||||
export async function getEnvContents(
|
||||
extensionConfig: ExtensionConfig,
|
||||
extensionId: string,
|
||||
workspaceDir: string,
|
||||
): Promise<Record<string, string>> {
|
||||
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
|
||||
return Promise.resolve({});
|
||||
@@ -185,6 +195,7 @@ export async function getEnvContents(
|
||||
extensionConfig,
|
||||
extensionId,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
workspaceDir,
|
||||
);
|
||||
|
||||
return { ...userSettings, ...workspaceSettings };
|
||||
@@ -196,6 +207,7 @@ export async function updateSetting(
|
||||
settingKey: string,
|
||||
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
||||
scope: ExtensionSettingScope,
|
||||
workspaceDir?: string,
|
||||
): Promise<void> {
|
||||
const { name: extensionName, settings } = extensionConfig;
|
||||
if (!settings || settings.length === 0) {
|
||||
@@ -214,7 +226,7 @@ export async function updateSetting(
|
||||
|
||||
const newValue = await requestSetting(settingToUpdate);
|
||||
const keychain = new KeychainTokenStorage(
|
||||
getKeychainStorageName(extensionName, extensionId, scope),
|
||||
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
|
||||
);
|
||||
|
||||
if (settingToUpdate.sensitive) {
|
||||
@@ -224,7 +236,7 @@ export async function updateSetting(
|
||||
|
||||
// For non-sensitive settings, we need to read the existing .env file,
|
||||
// update the value, and write it back, preserving any other values.
|
||||
const envFilePath = getEnvFilePath(extensionName, scope);
|
||||
const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir);
|
||||
let envContent = '';
|
||||
if (fsSync.existsSync(envFilePath)) {
|
||||
envContent = await fs.readFile(envFilePath, 'utf-8');
|
||||
@@ -302,13 +314,18 @@ async function clearSettings(
|
||||
export async function getMissingSettings(
|
||||
extensionConfig: ExtensionConfig,
|
||||
extensionId: string,
|
||||
workspaceDir: string,
|
||||
): Promise<ExtensionSetting[]> {
|
||||
const { settings } = extensionConfig;
|
||||
if (!settings || settings.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const existingSettings = await getEnvContents(extensionConfig, extensionId);
|
||||
const existingSettings = await getEnvContents(
|
||||
extensionConfig,
|
||||
extensionId,
|
||||
workspaceDir,
|
||||
);
|
||||
const missingSettings: ExtensionSetting[] = [];
|
||||
|
||||
for (const setting of settings) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as fs from 'node:fs';
|
||||
import { getMissingSettings } from './extensionSettings.js';
|
||||
import type { ExtensionConfig } from '../extension.js';
|
||||
import { ExtensionStorage } from './storage.js';
|
||||
import type { Settings } from '../settings.js';
|
||||
import {
|
||||
KeychainTokenStorage,
|
||||
debugLogger,
|
||||
@@ -154,7 +155,11 @@ describe('extensionUpdates', () => {
|
||||
);
|
||||
await userKeychain.setSecret('VAR2', 'val2');
|
||||
|
||||
const missing = await getMissingSettings(config, extensionId);
|
||||
const missing = await getMissingSettings(
|
||||
config,
|
||||
extensionId,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -166,7 +171,11 @@ describe('extensionUpdates', () => {
|
||||
};
|
||||
const extensionId = '12345';
|
||||
|
||||
const missing = await getMissingSettings(config, extensionId);
|
||||
const missing = await getMissingSettings(
|
||||
config,
|
||||
extensionId,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
expect(missing).toHaveLength(1);
|
||||
expect(missing[0].name).toBe('s1');
|
||||
});
|
||||
@@ -181,7 +190,11 @@ describe('extensionUpdates', () => {
|
||||
};
|
||||
const extensionId = '12345';
|
||||
|
||||
const missing = await getMissingSettings(config, extensionId);
|
||||
const missing = await getMissingSettings(
|
||||
config,
|
||||
extensionId,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
expect(missing).toHaveLength(1);
|
||||
expect(missing[0].name).toBe('s2');
|
||||
});
|
||||
@@ -201,7 +214,11 @@ describe('extensionUpdates', () => {
|
||||
);
|
||||
fs.writeFileSync(workspaceEnvPath, 'VAR1=val1');
|
||||
|
||||
const missing = await getMissingSettings(config, extensionId);
|
||||
const missing = await getMissingSettings(
|
||||
config,
|
||||
extensionId,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -229,8 +246,13 @@ describe('extensionUpdates', () => {
|
||||
|
||||
const manager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settings: { telemetry: {} } as any,
|
||||
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: { extensionConfig: true },
|
||||
} as unknown as Settings,
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: null, // Simulate non-interactive
|
||||
});
|
||||
@@ -266,26 +288,6 @@ describe('extensionUpdates', () => {
|
||||
vi.spyOn(fs.promises, 'writeFile').mockResolvedValue(undefined);
|
||||
vi.spyOn(fs.promises, 'rm').mockResolvedValue(undefined);
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false); // No hooks
|
||||
|
||||
// Mock copyExtension? It's imported.
|
||||
// We can rely on ignoring the failure if we mock enough.
|
||||
// Actually copyExtension is called. We need to mock it if it does real IO.
|
||||
// But we can just let it fail or mock fs.cp if it uses it.
|
||||
// Let's assume the other mocks cover the critical path to the warning.
|
||||
// Warning happens BEFORE copyExtension?
|
||||
// No, warning is after copyExtension usually.
|
||||
// But in my code:
|
||||
// const missingSettings = await getMissingSettings(...)
|
||||
// if (missingSettings.length > 0) debugLogger.warn(...)
|
||||
// ...
|
||||
// copyExtension(...)
|
||||
|
||||
// Wait, let's check extension-manager.ts order.
|
||||
// Line 303: getMissingSettings
|
||||
// Line 317: if (installMetadata.type === 'local' ...) copyExtension
|
||||
|
||||
// So getMissingSettings is called BEFORE copyExtension.
|
||||
|
||||
try {
|
||||
await manager.installOrUpdateExtension(installMetadata, previousConfig);
|
||||
} catch (_) {
|
||||
@@ -300,7 +302,7 @@ describe('extensionUpdates', () => {
|
||||
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
expect.stringContaining(
|
||||
'Please run "gemini extensions settings test-ext <setting-name>"',
|
||||
'Please run "gemini extensions config test-ext [setting-name]"',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
EXTENSION_SETTINGS_FILENAME,
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
} from './variables.js';
|
||||
import { Storage } from '@google/gemini-cli-core';
|
||||
import { Storage, homedir } from '@google/gemini-cli-core';
|
||||
|
||||
export class ExtensionStorage {
|
||||
private readonly extensionName: string;
|
||||
@@ -36,7 +36,7 @@ export class ExtensionStorage {
|
||||
}
|
||||
|
||||
static getUserExtensionsDir(): string {
|
||||
return new Storage(os.homedir()).getExtensionsDir();
|
||||
return new Storage(homedir()).getExtensionsDir();
|
||||
}
|
||||
|
||||
static async createTmpDir(): Promise<string> {
|
||||
|
||||
@@ -59,9 +59,20 @@ export enum Command {
|
||||
// App level bindings
|
||||
SHOW_ERROR_DETAILS = 'showErrorDetails',
|
||||
SHOW_FULL_TODOS = 'showFullTodos',
|
||||
TOGGLE_IDE_CONTEXT_DETAIL = 'toggleIDEContextDetail',
|
||||
SHOW_IDE_CONTEXT_DETAIL = 'showIDEContextDetail',
|
||||
TOGGLE_MARKDOWN = 'toggleMarkdown',
|
||||
TOGGLE_COPY_MODE = 'toggleCopyMode',
|
||||
TOGGLE_YOLO = 'toggleYolo',
|
||||
TOGGLE_AUTO_EDIT = 'toggleAutoEdit',
|
||||
UNDO = 'undo',
|
||||
REDO = 'redo',
|
||||
MOVE_LEFT = 'moveLeft',
|
||||
MOVE_RIGHT = 'moveRight',
|
||||
MOVE_WORD_LEFT = 'moveWordLeft',
|
||||
MOVE_WORD_RIGHT = 'moveWordRight',
|
||||
DELETE_CHAR_LEFT = 'deleteCharLeft',
|
||||
DELETE_CHAR_RIGHT = 'deleteCharRight',
|
||||
DELETE_WORD_FORWARD = 'deleteWordForward',
|
||||
QUIT = 'quit',
|
||||
EXIT = 'exit',
|
||||
SHOW_MORE_LINES = 'showMoreLines',
|
||||
@@ -70,7 +81,8 @@ export enum Command {
|
||||
REVERSE_SEARCH = 'reverseSearch',
|
||||
SUBMIT_REVERSE_SEARCH = 'submitReverseSearch',
|
||||
ACCEPT_SUGGESTION_REVERSE_SEARCH = 'acceptSuggestionReverseSearch',
|
||||
TOGGLE_SHELL_INPUT_FOCUS = 'toggleShellInputFocus',
|
||||
FOCUS_SHELL_INPUT = 'focusShellInput',
|
||||
UNFOCUS_SHELL_INPUT = 'unfocusShellInput',
|
||||
|
||||
// Suggestion expansion
|
||||
EXPAND_SUGGESTION = 'expandSuggestion',
|
||||
@@ -123,6 +135,37 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.DELETE_WORD_BACKWARD]: [
|
||||
{ key: 'backspace', ctrl: true },
|
||||
{ key: 'backspace', command: true },
|
||||
{ sequence: '\x7f', ctrl: true },
|
||||
{ sequence: '\x7f', command: true },
|
||||
{ key: 'w', ctrl: true },
|
||||
],
|
||||
[Command.MOVE_LEFT]: [
|
||||
{ key: 'left', ctrl: false, command: false },
|
||||
{ key: 'b', ctrl: true },
|
||||
],
|
||||
[Command.MOVE_RIGHT]: [
|
||||
{ key: 'right', ctrl: false, command: false },
|
||||
{ key: 'f', ctrl: true },
|
||||
],
|
||||
[Command.MOVE_WORD_LEFT]: [
|
||||
{ key: 'left', ctrl: true },
|
||||
{ key: 'left', command: true },
|
||||
{ key: 'b', command: true },
|
||||
],
|
||||
[Command.MOVE_WORD_RIGHT]: [
|
||||
{ key: 'right', ctrl: true },
|
||||
{ key: 'right', command: true },
|
||||
{ key: 'f', command: true },
|
||||
],
|
||||
[Command.DELETE_CHAR_LEFT]: [
|
||||
{ key: 'backspace' },
|
||||
{ sequence: '\x7f' },
|
||||
{ key: 'h', ctrl: true },
|
||||
],
|
||||
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
|
||||
[Command.DELETE_WORD_FORWARD]: [
|
||||
{ key: 'delete', ctrl: true },
|
||||
{ key: 'delete', command: true },
|
||||
],
|
||||
|
||||
// Screen control
|
||||
@@ -200,9 +243,13 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
// App level bindings
|
||||
[Command.SHOW_ERROR_DETAILS]: [{ key: 'f12' }],
|
||||
[Command.SHOW_FULL_TODOS]: [{ key: 't', ctrl: true }],
|
||||
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
|
||||
[Command.SHOW_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
|
||||
[Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }],
|
||||
[Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }],
|
||||
[Command.TOGGLE_YOLO]: [{ key: 'y', ctrl: true }],
|
||||
[Command.TOGGLE_AUTO_EDIT]: [{ key: 'tab', shift: true }],
|
||||
[Command.UNDO]: [{ key: 'z', ctrl: true, shift: false }],
|
||||
[Command.REDO]: [{ key: 'z', ctrl: true, shift: true }],
|
||||
[Command.QUIT]: [{ key: 'c', ctrl: true }],
|
||||
[Command.EXIT]: [{ key: 'd', ctrl: true }],
|
||||
[Command.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }],
|
||||
@@ -212,8 +259,8 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
// Note: original logic ONLY checked ctrl=false, ignored meta/shift/paste
|
||||
[Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }],
|
||||
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }],
|
||||
[Command.TOGGLE_SHELL_INPUT_FOCUS]: [{ key: 'f', ctrl: true }],
|
||||
|
||||
[Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }],
|
||||
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
|
||||
// Suggestion expansion
|
||||
[Command.EXPAND_SUGGESTION]: [{ key: 'right' }],
|
||||
[Command.COLLAPSE_SUGGESTION]: [{ key: 'left' }],
|
||||
@@ -234,7 +281,14 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
},
|
||||
{
|
||||
title: 'Cursor Movement',
|
||||
commands: [Command.HOME, Command.END],
|
||||
commands: [
|
||||
Command.HOME,
|
||||
Command.END,
|
||||
Command.MOVE_LEFT,
|
||||
Command.MOVE_RIGHT,
|
||||
Command.MOVE_WORD_LEFT,
|
||||
Command.MOVE_WORD_RIGHT,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Editing',
|
||||
@@ -243,6 +297,11 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
Command.KILL_LINE_LEFT,
|
||||
Command.CLEAR_INPUT,
|
||||
Command.DELETE_WORD_BACKWARD,
|
||||
Command.DELETE_WORD_FORWARD,
|
||||
Command.DELETE_CHAR_LEFT,
|
||||
Command.DELETE_CHAR_RIGHT,
|
||||
Command.UNDO,
|
||||
Command.REDO,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -302,11 +361,14 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
commands: [
|
||||
Command.SHOW_ERROR_DETAILS,
|
||||
Command.SHOW_FULL_TODOS,
|
||||
Command.TOGGLE_IDE_CONTEXT_DETAIL,
|
||||
Command.SHOW_IDE_CONTEXT_DETAIL,
|
||||
Command.TOGGLE_MARKDOWN,
|
||||
Command.TOGGLE_COPY_MODE,
|
||||
Command.TOGGLE_YOLO,
|
||||
Command.TOGGLE_AUTO_EDIT,
|
||||
Command.SHOW_MORE_LINES,
|
||||
Command.TOGGLE_SHELL_INPUT_FOCUS,
|
||||
Command.FOCUS_SHELL_INPUT,
|
||||
Command.UNFOCUS_SHELL_INPUT,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -323,10 +385,19 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.ESCAPE]: 'Dismiss dialogs or cancel the current focus.',
|
||||
[Command.HOME]: 'Move the cursor to the start of the line.',
|
||||
[Command.END]: 'Move the cursor to the end of the line.',
|
||||
[Command.MOVE_LEFT]: 'Move the cursor one character to the left.',
|
||||
[Command.MOVE_RIGHT]: 'Move the cursor one character to the right.',
|
||||
[Command.MOVE_WORD_LEFT]: 'Move the cursor one word to the left.',
|
||||
[Command.MOVE_WORD_RIGHT]: 'Move the cursor one word to the right.',
|
||||
[Command.KILL_LINE_RIGHT]: 'Delete from the cursor to the end of the line.',
|
||||
[Command.KILL_LINE_LEFT]: 'Delete from the cursor to the start of the line.',
|
||||
[Command.CLEAR_INPUT]: 'Clear all text in the input field.',
|
||||
[Command.DELETE_WORD_BACKWARD]: 'Delete the previous word.',
|
||||
[Command.DELETE_WORD_FORWARD]: 'Delete the next word.',
|
||||
[Command.DELETE_CHAR_LEFT]: 'Delete the character to the left.',
|
||||
[Command.DELETE_CHAR_RIGHT]: 'Delete the character to the right.',
|
||||
[Command.UNDO]: 'Undo the most recent text edit.',
|
||||
[Command.REDO]: 'Redo the most recent undone text edit.',
|
||||
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
|
||||
[Command.SCROLL_UP]: 'Scroll content up.',
|
||||
[Command.SCROLL_DOWN]: 'Scroll content down.',
|
||||
@@ -350,20 +421,22 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.PASTE_CLIPBOARD]: 'Paste from the clipboard.',
|
||||
[Command.SHOW_ERROR_DETAILS]: 'Toggle detailed error information.',
|
||||
[Command.SHOW_FULL_TODOS]: 'Toggle the full TODO list.',
|
||||
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: 'Toggle IDE context details.',
|
||||
[Command.SHOW_IDE_CONTEXT_DETAIL]: 'Show IDE context details.',
|
||||
[Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.',
|
||||
[Command.TOGGLE_COPY_MODE]:
|
||||
'Toggle copy mode when the terminal is using the alternate buffer.',
|
||||
[Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.',
|
||||
[Command.TOGGLE_AUTO_EDIT]: 'Toggle Auto Edit (auto-accept edits) mode.',
|
||||
[Command.QUIT]: 'Cancel the current request or quit the CLI.',
|
||||
[Command.EXIT]: 'Exit the CLI when the input buffer is empty.',
|
||||
[Command.SHOW_MORE_LINES]:
|
||||
'Expand a height-constrained response to show additional lines.',
|
||||
[Command.REVERSE_SEARCH]: 'Start reverse search through history.',
|
||||
[Command.SUBMIT_REVERSE_SEARCH]: 'Insert the selected reverse-search match.',
|
||||
[Command.SUBMIT_REVERSE_SEARCH]: 'Submit the selected reverse-search match.',
|
||||
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]:
|
||||
'Accept a suggestion while reverse searching.',
|
||||
[Command.TOGGLE_SHELL_INPUT_FOCUS]:
|
||||
'Toggle focus between the shell and Gemini input.',
|
||||
[Command.FOCUS_SHELL_INPUT]: 'Focus the shell input from the gemini input.',
|
||||
[Command.UNFOCUS_SHELL_INPUT]: 'Focus the Gemini input from the shell input.',
|
||||
[Command.EXPAND_SUGGESTION]: 'Expand an inline suggestion.',
|
||||
[Command.COLLAPSE_SUGGESTION]: 'Collapse an inline suggestion.',
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,16 +6,17 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { homedir, platform } from 'node:os';
|
||||
import { platform } from 'node:os';
|
||||
import * as dotenv from 'dotenv';
|
||||
import process from 'node:process';
|
||||
import {
|
||||
debugLogger,
|
||||
FatalConfigError,
|
||||
GEMINI_DIR,
|
||||
getErrorMessage,
|
||||
Storage,
|
||||
coreEvents,
|
||||
homedir,
|
||||
type GeminiCodeAssistSetting,
|
||||
} from '@google/gemini-cli-core';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { DefaultLight } from '../ui/themes/default-light.js';
|
||||
@@ -30,14 +31,12 @@ import {
|
||||
getSettingsSchema,
|
||||
} from './settingsSchema.js';
|
||||
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
|
||||
import { customDeepMerge, type MergeableObject } from '../utils/deepMerge.js';
|
||||
import { customDeepMerge } from '../utils/deepMerge.js';
|
||||
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
|
||||
import type { ExtensionManager } from './extension-manager.js';
|
||||
import {
|
||||
validateSettings,
|
||||
formatValidationError,
|
||||
} from './settings-validation.js';
|
||||
import { SettingPaths } from './settingPaths.js';
|
||||
|
||||
function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined {
|
||||
let current: SettingDefinition | undefined = undefined;
|
||||
@@ -66,79 +65,6 @@ export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
|
||||
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
|
||||
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
|
||||
|
||||
const MIGRATE_V2_OVERWRITE = true;
|
||||
|
||||
const MIGRATION_MAP: Record<string, string> = {
|
||||
accessibility: 'ui.accessibility',
|
||||
allowedTools: 'tools.allowed',
|
||||
allowMCPServers: 'mcp.allowed',
|
||||
autoAccept: 'tools.autoAccept',
|
||||
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
|
||||
bugCommand: 'advanced.bugCommand',
|
||||
chatCompression: 'model.compressionThreshold',
|
||||
checkpointing: 'general.checkpointing',
|
||||
coreTools: 'tools.core',
|
||||
contextFileName: 'context.fileName',
|
||||
customThemes: 'ui.customThemes',
|
||||
customWittyPhrases: 'ui.customWittyPhrases',
|
||||
debugKeystrokeLogging: 'general.debugKeystrokeLogging',
|
||||
disableAutoUpdate: 'general.disableAutoUpdate',
|
||||
disableUpdateNag: 'general.disableUpdateNag',
|
||||
dnsResolutionOrder: 'advanced.dnsResolutionOrder',
|
||||
enableHooks: 'tools.enableHooks',
|
||||
enablePromptCompletion: 'general.enablePromptCompletion',
|
||||
enforcedAuthType: 'security.auth.enforcedType',
|
||||
excludeTools: 'tools.exclude',
|
||||
excludeMCPServers: 'mcp.excluded',
|
||||
excludedProjectEnvVars: 'advanced.excludedEnvVars',
|
||||
experimentalSkills: 'experimental.skills',
|
||||
extensionManagement: 'experimental.extensionManagement',
|
||||
extensions: 'extensions',
|
||||
fileFiltering: 'context.fileFiltering',
|
||||
folderTrustFeature: 'security.folderTrust.featureEnabled',
|
||||
folderTrust: 'security.folderTrust.enabled',
|
||||
hasSeenIdeIntegrationNudge: 'ide.hasSeenNudge',
|
||||
hideWindowTitle: 'ui.hideWindowTitle',
|
||||
showStatusInTitle: 'ui.showStatusInTitle',
|
||||
hideTips: 'ui.hideTips',
|
||||
hideBanner: 'ui.hideBanner',
|
||||
hideFooter: 'ui.hideFooter',
|
||||
hideCWD: 'ui.footer.hideCWD',
|
||||
hideSandboxStatus: 'ui.footer.hideSandboxStatus',
|
||||
hideModelInfo: 'ui.footer.hideModelInfo',
|
||||
hideContextSummary: 'ui.hideContextSummary',
|
||||
showMemoryUsage: 'ui.showMemoryUsage',
|
||||
showLineNumbers: 'ui.showLineNumbers',
|
||||
showCitations: 'ui.showCitations',
|
||||
ideMode: 'ide.enabled',
|
||||
includeDirectories: 'context.includeDirectories',
|
||||
loadMemoryFromIncludeDirectories: 'context.loadFromIncludeDirectories',
|
||||
maxSessionTurns: 'model.maxSessionTurns',
|
||||
mcpServers: 'mcpServers',
|
||||
mcpServerCommand: 'mcp.serverCommand',
|
||||
memoryImportFormat: 'context.importFormat',
|
||||
memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs',
|
||||
model: 'model.name',
|
||||
preferredEditor: SettingPaths.General.PreferredEditor,
|
||||
retryFetchErrors: 'general.retryFetchErrors',
|
||||
sandbox: 'tools.sandbox',
|
||||
selectedAuthType: 'security.auth.selectedType',
|
||||
enableInteractiveShell: 'tools.shell.enableInteractiveShell',
|
||||
shellPager: 'tools.shell.pager',
|
||||
shellShowColor: 'tools.shell.showColor',
|
||||
shellInactivityTimeout: 'tools.shell.inactivityTimeout',
|
||||
skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
|
||||
summarizeToolOutput: 'model.summarizeToolOutput',
|
||||
telemetry: 'telemetry',
|
||||
theme: 'ui.theme',
|
||||
toolDiscoveryCommand: 'tools.discoveryCommand',
|
||||
toolCallCommand: 'tools.callCommand',
|
||||
usageStatisticsEnabled: 'privacy.usageStatisticsEnabled',
|
||||
useExternalAuth: 'security.auth.useExternal',
|
||||
useRipgrep: 'tools.useRipgrep',
|
||||
vimMode: 'general.vimMode',
|
||||
};
|
||||
|
||||
export function getSystemSettingsPath(): string {
|
||||
if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
|
||||
return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
|
||||
@@ -268,162 +194,6 @@ function setNestedProperty(
|
||||
current[lastKey] = value;
|
||||
}
|
||||
|
||||
export function needsMigration(settings: Record<string, unknown>): boolean {
|
||||
// A file needs migration if it contains any top-level key that is moved to a
|
||||
// nested location in V2.
|
||||
const hasV1Keys = Object.entries(MIGRATION_MAP).some(([v1Key, v2Path]) => {
|
||||
if (v1Key === v2Path || !(v1Key in settings)) {
|
||||
return false;
|
||||
}
|
||||
// If a key exists that is a V1 key and a V2 container (like 'model'),
|
||||
// we need to check the type. If it's an object, it's a V2 container and not
|
||||
// a V1 key that needs migration.
|
||||
if (
|
||||
KNOWN_V2_CONTAINERS.has(v1Key) &&
|
||||
typeof settings[v1Key] === 'object' &&
|
||||
settings[v1Key] !== null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return hasV1Keys;
|
||||
}
|
||||
|
||||
function migrateSettingsToV2(
|
||||
flatSettings: Record<string, unknown>,
|
||||
): Record<string, unknown> | null {
|
||||
if (!needsMigration(flatSettings)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v2Settings: Record<string, unknown> = {};
|
||||
const flatKeys = new Set(Object.keys(flatSettings));
|
||||
|
||||
for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
|
||||
if (flatKeys.has(oldKey)) {
|
||||
// If the key exists and is a V2 container (like 'model'), and the value is an object,
|
||||
// it is likely already migrated or partially migrated. We should not move it
|
||||
// to the mapped V2 path (e.g. 'model' -> 'model.name').
|
||||
// Instead, let it fall through to the "Carry over" section to be merged.
|
||||
if (
|
||||
KNOWN_V2_CONTAINERS.has(oldKey) &&
|
||||
typeof flatSettings[oldKey] === 'object' &&
|
||||
flatSettings[oldKey] !== null &&
|
||||
!Array.isArray(flatSettings[oldKey])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setNestedProperty(v2Settings, newPath, flatSettings[oldKey]);
|
||||
flatKeys.delete(oldKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve mcpServers at the top level
|
||||
if (flatSettings['mcpServers']) {
|
||||
v2Settings['mcpServers'] = flatSettings['mcpServers'];
|
||||
flatKeys.delete('mcpServers');
|
||||
}
|
||||
|
||||
// Carry over any unrecognized keys
|
||||
for (const remainingKey of flatKeys) {
|
||||
const existingValue = v2Settings[remainingKey];
|
||||
const newValue = flatSettings[remainingKey];
|
||||
|
||||
if (
|
||||
typeof existingValue === 'object' &&
|
||||
existingValue !== null &&
|
||||
!Array.isArray(existingValue) &&
|
||||
typeof newValue === 'object' &&
|
||||
newValue !== null &&
|
||||
!Array.isArray(newValue)
|
||||
) {
|
||||
const pathAwareGetStrategy = (path: string[]) =>
|
||||
getMergeStrategyForPath([remainingKey, ...path]);
|
||||
v2Settings[remainingKey] = customDeepMerge(
|
||||
pathAwareGetStrategy,
|
||||
{},
|
||||
existingValue as MergeableObject,
|
||||
newValue as MergeableObject,
|
||||
);
|
||||
} else {
|
||||
v2Settings[remainingKey] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
return v2Settings;
|
||||
}
|
||||
|
||||
function getNestedProperty(
|
||||
obj: Record<string, unknown>,
|
||||
path: string,
|
||||
): unknown {
|
||||
const keys = path.split('.');
|
||||
let current: unknown = obj;
|
||||
for (const key of keys) {
|
||||
if (typeof current !== 'object' || current === null || !(key in current)) {
|
||||
return undefined;
|
||||
}
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
const REVERSE_MIGRATION_MAP: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(MIGRATION_MAP).map(([key, value]) => [value, key]),
|
||||
);
|
||||
|
||||
// Dynamically determine the top-level keys from the V2 settings structure.
|
||||
const KNOWN_V2_CONTAINERS = new Set(
|
||||
Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]),
|
||||
);
|
||||
|
||||
export function migrateSettingsToV1(
|
||||
v2Settings: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const v1Settings: Record<string, unknown> = {};
|
||||
const v2Keys = new Set(Object.keys(v2Settings));
|
||||
|
||||
for (const [newPath, oldKey] of Object.entries(REVERSE_MIGRATION_MAP)) {
|
||||
const value = getNestedProperty(v2Settings, newPath);
|
||||
if (value !== undefined) {
|
||||
v1Settings[oldKey] = value;
|
||||
v2Keys.delete(newPath.split('.')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve mcpServers at the top level
|
||||
if (v2Settings['mcpServers']) {
|
||||
v1Settings['mcpServers'] = v2Settings['mcpServers'];
|
||||
v2Keys.delete('mcpServers');
|
||||
}
|
||||
|
||||
// Carry over any unrecognized keys
|
||||
for (const remainingKey of v2Keys) {
|
||||
const value = v2Settings[remainingKey];
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't carry over empty objects that were just containers for migrated settings.
|
||||
if (
|
||||
KNOWN_V2_CONTAINERS.has(remainingKey) &&
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.keys(value).length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
v1Settings[remainingKey] = value;
|
||||
}
|
||||
|
||||
return v1Settings;
|
||||
}
|
||||
|
||||
export function getDefaultsFromSchema(
|
||||
schema: SettingsSchema = getSettingsSchema(),
|
||||
): Settings {
|
||||
@@ -476,7 +246,6 @@ export class LoadedSettings {
|
||||
user: SettingsFile,
|
||||
workspace: SettingsFile,
|
||||
isTrusted: boolean,
|
||||
migratedInMemoryScopes: Set<SettingScope>,
|
||||
errors: SettingsError[] = [],
|
||||
) {
|
||||
this.system = system;
|
||||
@@ -484,7 +253,6 @@ export class LoadedSettings {
|
||||
this.user = user;
|
||||
this.workspace = workspace;
|
||||
this.isTrusted = isTrusted;
|
||||
this.migratedInMemoryScopes = migratedInMemoryScopes;
|
||||
this.errors = errors;
|
||||
this._merged = this.computeMergedSettings();
|
||||
}
|
||||
@@ -494,23 +262,40 @@ export class LoadedSettings {
|
||||
readonly user: SettingsFile;
|
||||
readonly workspace: SettingsFile;
|
||||
readonly isTrusted: boolean;
|
||||
readonly migratedInMemoryScopes: Set<SettingScope>;
|
||||
readonly errors: SettingsError[];
|
||||
|
||||
private _merged: Settings;
|
||||
private _remoteAdminSettings: Partial<Settings> | undefined;
|
||||
|
||||
get merged(): Settings {
|
||||
return this._merged;
|
||||
}
|
||||
|
||||
private computeMergedSettings(): Settings {
|
||||
return mergeSettings(
|
||||
const merged = mergeSettings(
|
||||
this.system.settings,
|
||||
this.systemDefaults.settings,
|
||||
this.user.settings,
|
||||
this.workspace.settings,
|
||||
this.isTrusted,
|
||||
);
|
||||
|
||||
// Remote admin settings always take precedence and file-based admin settings
|
||||
// are ignored.
|
||||
const adminSettingSchema = getSettingsSchema().admin;
|
||||
if (adminSettingSchema?.properties) {
|
||||
const adminSchema = adminSettingSchema.properties;
|
||||
const adminDefaults = getDefaultsFromSchema(adminSchema);
|
||||
|
||||
// The final admin settings are the defaults overridden by remote settings.
|
||||
// Any admin settings from files are ignored.
|
||||
merged.admin = customDeepMerge(
|
||||
(path: string[]) => getMergeStrategyForPath(['admin', ...path]),
|
||||
adminDefaults,
|
||||
this._remoteAdminSettings?.admin ?? {},
|
||||
) as Settings['admin'];
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
forScope(scope: LoadableSettingScope): SettingsFile {
|
||||
@@ -536,6 +321,27 @@ export class LoadedSettings {
|
||||
saveSettings(settingsFile);
|
||||
coreEvents.emitSettingsChanged();
|
||||
}
|
||||
|
||||
setRemoteAdminSettings(remoteSettings: GeminiCodeAssistSetting): void {
|
||||
const admin: Settings['admin'] = {};
|
||||
const { secureModeEnabled, mcpSetting, cliFeatureSetting } = remoteSettings;
|
||||
|
||||
if (secureModeEnabled !== undefined) {
|
||||
admin.secureModeEnabled = secureModeEnabled;
|
||||
}
|
||||
|
||||
if (mcpSetting?.mcpEnabled !== undefined) {
|
||||
admin.mcp = { enabled: mcpSetting.mcpEnabled };
|
||||
}
|
||||
|
||||
const extensionsSetting = cliFeatureSetting?.extensionsSetting;
|
||||
if (extensionsSetting?.extensionsEnabled !== undefined) {
|
||||
admin.extensions = { enabled: extensionsSetting.extensionsEnabled };
|
||||
}
|
||||
|
||||
this._remoteAdminSettings = { admin };
|
||||
this._merged = this.computeMergedSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function findEnvFile(startDir: string): string | null {
|
||||
@@ -645,7 +451,6 @@ export function loadSettings(
|
||||
const settingsErrors: SettingsError[] = [];
|
||||
const systemSettingsPath = getSystemSettingsPath();
|
||||
const systemDefaultsPath = getSystemDefaultsPath();
|
||||
const migratedInMemoryScopes = new Set<SettingScope>();
|
||||
|
||||
// Resolve paths to their canonical representation to handle symlinks
|
||||
const resolvedWorkspaceDir = path.resolve(workspaceDir);
|
||||
@@ -666,10 +471,7 @@ export function loadSettings(
|
||||
workspaceDir,
|
||||
).getWorkspaceSettingsPath();
|
||||
|
||||
const loadAndMigrate = (
|
||||
filePath: string,
|
||||
scope: SettingScope,
|
||||
): { settings: Settings; rawJson?: string } => {
|
||||
const load = (filePath: string): { settings: Settings; rawJson?: string } => {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
@@ -688,33 +490,9 @@ export function loadSettings(
|
||||
return { settings: {} };
|
||||
}
|
||||
|
||||
let settingsObject = rawSettings as Record<string, unknown>;
|
||||
if (needsMigration(settingsObject)) {
|
||||
const migratedSettings = migrateSettingsToV2(settingsObject);
|
||||
if (migratedSettings) {
|
||||
if (MIGRATE_V2_OVERWRITE) {
|
||||
try {
|
||||
fs.renameSync(filePath, `${filePath}.orig`);
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
JSON.stringify(migratedSettings, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
} catch (e) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'Failed to migrate settings file.',
|
||||
e,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
migratedInMemoryScopes.add(scope);
|
||||
}
|
||||
settingsObject = migratedSettings;
|
||||
}
|
||||
}
|
||||
const settingsObject = rawSettings as Record<string, unknown>;
|
||||
|
||||
// Validate settings structure with Zod after migration
|
||||
// Validate settings structure with Zod
|
||||
const validationResult = validateSettings(settingsObject);
|
||||
if (!validationResult.success && validationResult.error) {
|
||||
const errorMessage = formatValidationError(
|
||||
@@ -740,22 +518,16 @@ export function loadSettings(
|
||||
return { settings: {} };
|
||||
};
|
||||
|
||||
const systemResult = loadAndMigrate(systemSettingsPath, SettingScope.System);
|
||||
const systemDefaultsResult = loadAndMigrate(
|
||||
systemDefaultsPath,
|
||||
SettingScope.SystemDefaults,
|
||||
);
|
||||
const userResult = loadAndMigrate(USER_SETTINGS_PATH, SettingScope.User);
|
||||
const systemResult = load(systemSettingsPath);
|
||||
const systemDefaultsResult = load(systemDefaultsPath);
|
||||
const userResult = load(USER_SETTINGS_PATH);
|
||||
|
||||
let workspaceResult: { settings: Settings; rawJson?: string } = {
|
||||
settings: {} as Settings,
|
||||
rawJson: undefined,
|
||||
};
|
||||
if (realWorkspaceDir !== realHomeDir) {
|
||||
workspaceResult = loadAndMigrate(
|
||||
workspaceSettingsPath,
|
||||
SettingScope.Workspace,
|
||||
);
|
||||
workspaceResult = load(workspaceSettingsPath);
|
||||
}
|
||||
|
||||
const systemOriginalSettings = structuredClone(systemResult.settings);
|
||||
@@ -843,37 +615,10 @@ export function loadSettings(
|
||||
rawJson: workspaceResult.rawJson,
|
||||
},
|
||||
isTrusted,
|
||||
migratedInMemoryScopes,
|
||||
settingsErrors,
|
||||
);
|
||||
}
|
||||
|
||||
export function migrateDeprecatedSettings(
|
||||
loadedSettings: LoadedSettings,
|
||||
extensionManager: ExtensionManager,
|
||||
): void {
|
||||
const processScope = (scope: LoadableSettingScope) => {
|
||||
const settings = loadedSettings.forScope(scope).settings;
|
||||
if (settings.extensions?.disabled) {
|
||||
debugLogger.log(
|
||||
`Migrating deprecated extensions.disabled settings from ${scope} settings...`,
|
||||
);
|
||||
for (const extension of settings.extensions.disabled ?? []) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
extensionManager.disableExtension(extension, scope);
|
||||
}
|
||||
|
||||
const newExtensionsValue = { ...settings.extensions };
|
||||
newExtensionsValue.disabled = undefined;
|
||||
|
||||
loadedSettings.setValue(scope, 'extensions', newExtensionsValue);
|
||||
}
|
||||
};
|
||||
|
||||
processScope(SettingScope.User);
|
||||
processScope(SettingScope.Workspace);
|
||||
}
|
||||
|
||||
export function saveSettings(settingsFile: SettingsFile): void {
|
||||
try {
|
||||
// Ensure the directory exists
|
||||
@@ -882,12 +627,7 @@ export function saveSettings(settingsFile: SettingsFile): void {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
let settingsToSave = settingsFile.originalSettings;
|
||||
if (!MIGRATE_V2_OVERWRITE) {
|
||||
settingsToSave = migrateSettingsToV1(
|
||||
settingsToSave as Record<string, unknown>,
|
||||
) as Settings;
|
||||
}
|
||||
const settingsToSave = settingsFile.originalSettings;
|
||||
|
||||
// Use the format-preserving update function
|
||||
updateSettingsFilePreservingFormat(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user