Compare commits

..

10 Commits

Author SHA1 Message Date
gemini-cli-robot 3d5bdc0525 chore(release): v0.40.0 2026-04-28 20:16:59 +00:00
gemini-cli-robot cefbd08f7a chore(release): v0.40.0-preview.5 2026-04-28 18:02:43 +00:00
gemini-cli-robot 7fd336f5fc fix(patch): cherry-pick 54b7586 to release/v0.40.0-preview.4-pr-26066 [CONFLICTS] (#26124)
Co-authored-by: David Pierce <davidapierce@google.com>
2026-04-28 10:21:16 -07:00
gemini-cli-robot 5116b5ac05 chore(release): v0.40.0-preview.4 2026-04-25 00:27:22 +00:00
gemini-cli-robot 31bdf112d2 fix(patch): cherry-pick 048bf6e to release/v0.40.0-preview.3-pr-25941 to patch version v0.40.0-preview.3 and create version 0.40.0-preview.4 (#25942)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-04-24 14:55:45 -07:00
gemini-cli-robot 8482eccf5d chore(release): v0.40.0-preview.3 2026-04-24 02:02:54 +00:00
Emily Hedlund 61c6a2adf8 update FatalUntrustedWorkspaceError message to include doc link (#25874) 2026-04-23 16:57:56 -07:00
Gal Zahavi c48c419f3b feat(core): enhance shell command validation and add core tools allowlist (#25720)
Co-authored-by: David Pierce <davidapierce@google.com>
Co-authored-by: Keith Schaab <keithsc@google.com>
Co-authored-by: Keith Schaab <keith.schaab@gmail.com>
Co-authored-by: Emily Hedlund <ehedlund@google.com>
2026-04-23 16:57:52 -07:00
Emily Hedlund a9b630f808 feat(cli): secure .env loading and enforce workspace trust in headless mode (#25814)
Co-authored-by: galz10 <galzahavi@google.com>
Co-authored-by: davidapierce <davidapierce@google.com>
2026-04-23 16:57:47 -07:00
gemini-cli-robot ee7e037c93 chore(release): v0.40.0-preview.2 2026-04-23 04:06:09 +00:00
71 changed files with 136 additions and 2309 deletions
+1 -2
View File
@@ -2,8 +2,7 @@
"experimental": {
"extensionReloading": true,
"modelSteering": true,
"autoMemory": true,
"gemma": true
"autoMemory": true
},
"general": {
"devtools": true
@@ -1,45 +0,0 @@
name: '🧠 Gemini CLI Bot: Brain'
on:
schedule:
- cron: '0 0 * * *' # Every 24 hours
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
permissions:
contents: 'write'
issues: 'write'
pull-requests: 'write'
jobs:
brain:
name: 'Brain (Reasoning Layer)'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 0
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build Gemini CLI'
run: 'npm run bundle'
- name: 'Download Previous Metrics'
uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' # ratchet:actions/download-artifact@v4
with:
name: 'metrics-before'
path: 'tools/gemini-cli-bot/history/'
continue-on-error: true
@@ -1,59 +0,0 @@
name: '🔄 Gemini CLI Bot: Pulse'
on:
schedule:
- cron: '*/30 * * * *' # Every 30 minutes
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
permissions:
contents: 'write'
issues: 'write'
pull-requests: 'write'
jobs:
pulse:
name: 'Pulse (Reflex Layer)'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 0
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Collect Metrics'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: 'npm run metrics'
- name: 'Archive Metrics'
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'metrics-before'
path: 'metrics-before.csv'
- name: 'Run Reflex Processes'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
if [ -d "tools/gemini-cli-bot/processes/scripts" ] && [ "$(ls -A tools/gemini-cli-bot/processes/scripts)" ]; then
for script in tools/gemini-cli-bot/processes/scripts/*.ts; do
echo "Running reflex script: $script"
npx tsx "$script"
done
else
echo "No reflex scripts found."
fi
-2
View File
@@ -371,8 +371,6 @@ for planned features and priorities.
## 📖 Resources
- **[Free Course](https://learn.deeplearning.ai/courses/gemini-cli-code-and-create-with-an-open-source-agent/information)** -
Learn the basics.
- **[Official Roadmap](./ROADMAP.md)** - See what's coming next.
- **[Changelog](https://www.geminicli.com/docs/changelogs)** - See recent
notable updates.
-1
View File
@@ -163,7 +163,6 @@ they appear in the UI.
| UI Label | Setting | Description | Default |
| ---------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Gemma Models | `experimental.gemma` | Enable access to Gemma 4 models (experimental). | `false` |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
+7 -52
View File
@@ -563,18 +563,6 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-2.5-flash-lite"
}
},
"gemma-4-31b-it": {
"extends": "chat-base-3",
"modelConfig": {
"model": "gemma-4-31b-it"
}
},
"gemma-4-26b-a4b-it": {
"extends": "chat-base-3",
"modelConfig": {
"model": "gemma-4-26b-a4b-it"
}
},
"gemini-2.5-flash-base": {
"extends": "base",
"modelConfig": {
@@ -846,28 +834,6 @@ their corresponding top-level category object in your `settings.json` file.
"multimodalToolUse": false
}
},
"gemma-4-31b-it": {
"displayName": "gemma-4-31b-it",
"tier": "custom",
"family": "gemma-4",
"isPreview": false,
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": false
}
},
"gemma-4-26b-a4b-it": {
"displayName": "gemma-4-26b-a4b-it",
"tier": "custom",
"family": "gemma-4",
"isPreview": false,
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": false
}
},
"auto": {
"tier": "auto",
"isPreview": true,
@@ -938,12 +904,6 @@ their corresponding top-level category object in your `settings.json` file.
```json
{
"gemma-4-31b-it": {
"default": "gemma-4-31b-it"
},
"gemma-4-26b-a4b-it": {
"default": "gemma-4-26b-a4b-it"
},
"gemini-3.1-pro-preview": {
"default": "gemini-3.1-pro-preview",
"contexts": [
@@ -1191,7 +1151,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "terminal",
"transient": "sticky_retry",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1207,7 +1167,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "terminal",
"transient": "sticky_retry",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1224,7 +1184,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "terminal",
"transient": "sticky_retry",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1240,7 +1200,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "terminal",
"transient": "sticky_retry",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1257,7 +1217,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "terminal",
"transient": "sticky_retry",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1272,7 +1232,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "terminal",
"transient": "sticky_retry",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1288,7 +1248,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "terminal",
"transient": "sticky_retry",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1686,11 +1646,6 @@ their corresponding top-level category object in your `settings.json` file.
#### `experimental`
- **`experimental.gemma`** (boolean):
- **Description:** Enable access to Gemma 4 models (experimental).
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.adk.agentSessionNoninteractiveEnabled`** (boolean):
- **Description:** Enable non-interactive agent sessions.
- **Default:** `false`
+3 -3
View File
@@ -17,7 +17,7 @@ describe('Hierarchical Memory', () => {
params: {
settings: {
security: {
folderTrust: { enabled: false },
folderTrust: { enabled: true },
},
},
},
@@ -55,7 +55,7 @@ What is my favorite fruit? Tell me just the name of the fruit.`,
params: {
settings: {
security: {
folderTrust: { enabled: false },
folderTrust: { enabled: true },
},
},
},
@@ -96,7 +96,7 @@ Provide the answer as an XML block like this:
params: {
settings: {
security: {
folderTrust: { enabled: false },
folderTrust: { enabled: true },
},
},
},
+13 -398
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"workspaces": [
"packages/*"
],
@@ -991,37 +991,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/config-array/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@eslint/config-array/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz",
@@ -1069,24 +1038,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -1100,19 +1051,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@eslint/js": {
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
@@ -3795,37 +3733,6 @@
"path-browserify": "^1.0.1"
}
},
"node_modules/@ts-morph/common/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/@ts-morph/common/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@ts-morph/common/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@@ -4986,13 +4893,6 @@
"win32"
]
},
"node_modules/@vscode/vsce/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/@vscode/vsce/node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -5032,30 +4932,6 @@
"node": ">=4"
}
},
"node_modules/@vscode/vsce/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@vscode/vsce/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -6553,13 +6429,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@@ -7075,23 +6944,6 @@
"sprintf-js": "~1.0.2"
}
},
"node_modules/depcheck/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/depcheck/node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/depcheck/node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@@ -7151,22 +7003,6 @@
"node": ">=6"
}
},
"node_modules/depcheck/node_modules/minimatch": {
"version": "7.4.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz",
"integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/depcheck/node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -8057,24 +7893,6 @@
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
}
},
"node_modules/eslint-plugin-import/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint-plugin-import/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
@@ -8085,19 +7903,6 @@
"ms": "^2.1.1"
}
},
"node_modules/eslint-plugin-import/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint-plugin-import/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -8154,37 +7959,6 @@
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-react/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint-plugin-react/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint-plugin-react/node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@@ -8243,37 +8017,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -11914,12 +11657,12 @@
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -12100,37 +11843,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/multimatch/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/multimatch/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/multimatch/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/mute-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
@@ -12371,24 +12083,6 @@
"node": ">=4"
}
},
"node_modules/npm-run-all/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/npm-run-all/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/npm-run-all/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -12438,19 +12132,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/npm-run-all/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/npm-run-all/node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@@ -15817,39 +15498,6 @@
"node": ">=18"
}
},
"node_modules/test-exclude/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/test-exclude/node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/test-exclude/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
@@ -16608,23 +16256,6 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/typescript-eslint/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript-eslint/node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/typescript-eslint/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -16635,22 +16266,6 @@
"node": ">= 4"
}
},
"node_modules/typescript-eslint/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@@ -18077,7 +17692,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "^7.16.0",
@@ -18206,7 +17821,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
@@ -18354,7 +17969,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -18664,7 +18279,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -18679,7 +18294,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18710,7 +18325,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18742,7 +18357,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"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.41.0-nightly.20260423.gaa05b4583"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.40.0"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -63,7 +63,6 @@
"lint:all": "node scripts/lint.js",
"format": "prettier --experimental-cli --write .",
"typecheck": "npm run typecheck --workspaces --if-present && tsc -b evals/tsconfig.json integration-tests/tsconfig.json memory-tests/tsconfig.json",
"metrics": "tsx tools/gemini-cli-bot/metrics/index.ts",
"preflight": "npm run clean && npm ci && npm run format && npm run build && npm run lint:ci && npm run typecheck && npm run test:ci",
"prepare": "husky && npm run bundle",
"prepare:package": "node scripts/prepare-package.js",
@@ -82,7 +81,8 @@
"glob": "^12.0.0",
"node-domexception": "npm:empty@^0.10.1",
"prebuild-install": "npm:nop@1.0.0",
"cross-spawn": "^7.0.6"
"cross-spawn": "^7.0.6",
"minimatch": "^10.2.2"
},
"bin": {
"gemini": "bundle/gemini.js"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -112,7 +112,6 @@ export function createMockConfig(
}),
isContextManagementEnabled: vi.fn().mockReturnValue(false),
getContextManagementConfig: vi.fn().mockReturnValue({ enabled: false }),
getExperimentalGemma: vi.fn().mockReturnValue(false),
...overrides,
} as unknown as Config;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -27,7 +27,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.0-nightly.20260423.gaa05b4583"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.40.0"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
-12
View File
@@ -3055,18 +3055,6 @@ describe('loadCliConfig gemmaModelRouter', () => {
expect(gemmaSettings.classifier?.model).toBe('custom-gemma');
});
it('should load experimental.gemma setting from merged settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
experimental: {
gemma: true,
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getExperimentalGemma()).toBe(true);
});
it('should handle partial gemmaModelRouter settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
-1
View File
@@ -1011,7 +1011,6 @@ export async function loadCliConfig(
experimentalJitContext,
experimentalMemoryV2: settings.experimental?.memoryV2,
experimentalAutoMemory: settings.experimental?.autoMemory,
experimentalGemma: settings.experimental?.gemma,
contextManagement,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration:
@@ -2052,15 +2052,6 @@ const SETTINGS_SCHEMA = {
description: 'Setting to enable experimental features',
showInDialog: false,
properties: {
gemma: {
type: 'boolean',
label: 'Gemma Models',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable access to Gemma 4 models (experimental).',
showInDialog: true,
},
adk: {
type: 'object',
label: 'ADK',
-1
View File
@@ -306,7 +306,6 @@ describe('gemini.tsx main function cleanup', () => {
getMessageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: vi.fn(() => true),
getHookSystem: vi.fn(() => undefined),
getExperimentalGemma: vi.fn(() => false),
initialize: vi.fn(),
storage: { initialize: vi.fn().mockResolvedValue(undefined) },
getContentGeneratorConfig: vi.fn(),
@@ -89,7 +89,6 @@ describe('ShellProcessor', () => {
getPolicyEngine: vi.fn().mockReturnValue({
check: mockPolicyEngineCheck,
}),
getExperimentalGemma: vi.fn().mockReturnValue(false),
get config() {
return this as unknown as Config;
},
@@ -168,7 +168,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getAdminSkillsEnabled: vi.fn().mockReturnValue(false),
getDisabledSkills: vi.fn().mockReturnValue([]),
getExperimentalJitContext: vi.fn().mockReturnValue(false),
getExperimentalGemma: vi.fn().mockReturnValue(false),
getMemoryBoundaryMarkers: vi.fn().mockReturnValue(['.git']),
getTerminalBackground: vi.fn().mockReturnValue(undefined),
getEmbeddingModel: vi.fn().mockReturnValue('embedding-model'),
@@ -65,7 +65,6 @@ describe('<ModelDialog />', () => {
getGemini31FlashLiteLaunchedSync: () => boolean;
getProModelNoAccess: () => Promise<boolean>;
getProModelNoAccessSync: () => boolean;
getExperimentalGemma: () => boolean;
getLastRetrievedQuota: () =>
| {
buckets: Array<{
@@ -86,7 +85,6 @@ describe('<ModelDialog />', () => {
getGemini31FlashLiteLaunchedSync: mockGetGemini31FlashLiteLaunchedSync,
getProModelNoAccess: mockGetProModelNoAccess,
getProModelNoAccessSync: mockGetProModelNoAccessSync,
getExperimentalGemma: () => false,
getLastRetrievedQuota: () => ({ buckets: [] }),
getSessionId: () => 'test-session-id',
};
+4 -23
View File
@@ -19,8 +19,6 @@ import {
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
ModelSlashCommandEvent,
logModelSlashCommand,
getDisplayString,
@@ -224,9 +222,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
}
// --- LEGACY PATH ---
const showGemmaModels = config?.getExperimentalGemma() ?? false;
const options = [
const list = [
{
value: DEFAULT_GEMINI_MODEL,
title: getDisplayString(DEFAULT_GEMINI_MODEL),
@@ -244,21 +240,6 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
},
];
if (showGemmaModels) {
options.push(
{
value: GEMMA_4_31B_IT_MODEL,
title: getDisplayString(GEMMA_4_31B_IT_MODEL),
key: GEMMA_4_31B_IT_MODEL,
},
{
value: GEMMA_4_26B_A4B_IT_MODEL,
title: getDisplayString(GEMMA_4_26B_A4B_IT_MODEL),
key: GEMMA_4_26B_A4B_IT_MODEL,
},
);
}
if (shouldShowPreviewModels) {
const previewProModel = useGemini31
? PREVIEW_GEMINI_3_1_MODEL
@@ -289,15 +270,15 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
});
}
options.unshift(...previewOptions);
list.unshift(...previewOptions);
}
if (!hasAccessToProModel) {
// Filter out all Pro models for free tier
return options.filter((option) => !isProModel(option.value));
return list.filter((option) => !isProModel(option.value));
}
return options;
return list;
}, [
shouldShowPreviewModels,
useGemini31,
@@ -86,7 +86,6 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getProjectTempDir: () => '/tmp/test',
},
getSessionId: () => 'default-session-id',
getExperimentalGemma: () => false,
...overrides,
}) as Config;
@@ -44,7 +44,7 @@ enum TerminalKeys {
LEFT_ARROW = '\u001B[D',
RIGHT_ARROW = '\u001B[C',
ESCAPE = '\u001B',
BACKSPACE = '\x7f',
BACKSPACE = '\u0008',
CTRL_P = '\u0010',
CTRL_N = '\u000E',
}
@@ -1,56 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderWithProviders } from '../../../test-utils/render.js';
import { HintMessage } from './HintMessage.js';
import { describe, it, expect, vi } from 'vitest';
import { makeFakeConfig } from '@google/gemini-cli-core';
describe('HintMessage', () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('renders normal hint message with correct prefix', async () => {
const { lastFrame, unmount } = await renderWithProviders(
<HintMessage text="Try this instead" />,
{ width: 80 },
);
const output = lastFrame();
expect(output).toContain('💡');
expect(output).toContain('Steering Hint: Try this instead');
unmount();
});
describe('with NO_COLOR set', () => {
beforeEach(() => {
vi.stubEnv('NO_COLOR', '1');
});
it('uses margins instead of background blocks when NO_COLOR is set', async () => {
const { lastFrame, unmount } = await renderWithProviders(
<HintMessage text="Try this instead" />,
{ width: 80, config: makeFakeConfig({ useBackgroundColor: true }) },
);
const output = lastFrame();
// In NO_COLOR mode, the block characters (▄/▀) should NOT be present.
expect(output).not.toContain('▄');
expect(output).not.toContain('▀');
const lines = output.split('\n').filter((l) => l.trim() !== '');
expect(lines).toHaveLength(1);
expect(lines[0]).toContain('💡');
expect(lines[0]).toContain('Steering Hint: Try this instead');
expect(output).toMatchSnapshot();
unmount();
});
});
});
@@ -19,9 +19,7 @@ export const HintMessage: React.FC<HintMessageProps> = ({ text }) => {
const prefix = '💡 ';
const prefixWidth = prefix.length;
const config = useConfig();
const useBackgroundColorSetting = config.getUseBackgroundColor();
const useBackgroundColor =
useBackgroundColorSetting && !!theme.background.message;
const useBackgroundColor = config.getUseBackgroundColor();
return (
<HalfLinePaddedBox
@@ -7,7 +7,6 @@
import { renderWithProviders } from '../../../test-utils/render.js';
import { UserMessage } from './UserMessage.js';
import { describe, it, expect, vi } from 'vitest';
import { makeFakeConfig } from '@google/gemini-cli-core';
// Mock the commandUtils to control isSlashCommand behavior
vi.mock('../../utils/commandUtils.js', () => ({
@@ -15,11 +14,6 @@ vi.mock('../../utils/commandUtils.js', () => ({
}));
describe('UserMessage', () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('renders normal user message with correct prefix', async () => {
const { lastFrame, unmount } = await renderWithProviders(
<UserMessage text="Hello Gemini" width={80} />,
@@ -66,32 +60,4 @@ describe('UserMessage', () => {
expect(output).toMatchSnapshot();
unmount();
});
describe('with NO_COLOR set', () => {
beforeEach(() => {
vi.stubEnv('NO_COLOR', '1');
});
it('uses margins instead of background blocks when NO_COLOR is set', async () => {
const { lastFrame, unmount } = await renderWithProviders(
<UserMessage text="Hello Gemini" width={80} />,
{ width: 80, config: makeFakeConfig({ useBackgroundColor: true }) },
);
const output = lastFrame();
// In NO_COLOR mode, the block characters (▄/▀) should NOT be present.
expect(output).not.toContain('▄');
expect(output).not.toContain('▀');
// There should be empty lines above and below the message due to marginY={1}.
// lastFrame() returns the full buffer, so we can check for leading/trailing newlines or empty lines.
const lines = output.split('\n').filter((l) => l.trim() !== '');
expect(lines).toHaveLength(1);
expect(lines[0]).toContain('> Hello Gemini');
expect(output).toMatchSnapshot();
unmount();
});
});
});
@@ -27,9 +27,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => {
const prefixWidth = prefix.length;
const isSlashCommand = checkIsSlashCommand(text);
const config = useConfig();
const useBackgroundColorSetting = config.getUseBackgroundColor();
const useBackgroundColor =
useBackgroundColorSetting && !!theme.background.message;
const useBackgroundColor = config.getUseBackgroundColor();
const textColor = isSlashCommand ? theme.text.accent : theme.text.primary;
@@ -1,54 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderWithProviders } from '../../../test-utils/render.js';
import { UserShellMessage } from './UserShellMessage.js';
import { describe, it, expect, vi } from 'vitest';
import { makeFakeConfig } from '@google/gemini-cli-core';
describe('UserShellMessage', () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('renders normal shell message with correct prefix', async () => {
const { lastFrame, unmount } = await renderWithProviders(
<UserShellMessage text="ls -la" width={80} />,
{ width: 80 },
);
const output = lastFrame();
expect(output).toContain('$ ls -la');
unmount();
});
describe('with NO_COLOR set', () => {
beforeEach(() => {
vi.stubEnv('NO_COLOR', '1');
});
it('uses margins instead of background blocks when NO_COLOR is set', async () => {
const { lastFrame, unmount } = await renderWithProviders(
<UserShellMessage text="ls -la" width={80} />,
{ width: 80, config: makeFakeConfig({ useBackgroundColor: true }) },
);
const output = lastFrame();
// In NO_COLOR mode, the block characters (▄/▀) should NOT be present.
expect(output).not.toContain('▄');
expect(output).not.toContain('▀');
const lines = output.split('\n').filter((l) => l.trim() !== '');
expect(lines).toHaveLength(1);
expect(lines[0]).toContain('$ ls -la');
expect(output).toMatchSnapshot();
unmount();
});
});
});
@@ -20,9 +20,7 @@ export const UserShellMessage: React.FC<UserShellMessageProps> = ({
width,
}) => {
const config = useConfig();
const useBackgroundColorSetting = config.getUseBackgroundColor();
const useBackgroundColor =
useBackgroundColorSetting && !!theme.background.message;
const useBackgroundColor = config.getUseBackgroundColor();
// Remove leading '!' if present, as App.tsx adds it for the processor.
const commandToDisplay = text.startsWith('!') ? text.substring(1) : text;
@@ -1,7 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`HintMessage > with NO_COLOR set > uses margins instead of background blocks when NO_COLOR is set 1`] = `
"
💡 Steering Hint: Try this instead
"
`;
@@ -28,9 +28,3 @@ exports[`UserMessage > transforms image paths in user message 1`] = `
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
"
`;
exports[`UserMessage > with NO_COLOR set > uses margins instead of background blocks when NO_COLOR is set 1`] = `
"
> Hello Gemini
"
`;
@@ -1,7 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`UserShellMessage > with NO_COLOR set > uses margins instead of background blocks when NO_COLOR is set 1`] = `
"
$ ls -la
"
`;
@@ -24,7 +24,7 @@ enum TerminalKeys {
LEFT_ARROW = '\u001B[D',
RIGHT_ARROW = '\u001B[C',
ESCAPE = '\u001B',
BACKSPACE = '\x7f',
BACKSPACE = '\u0008',
CTRL_L = '\u000C',
}
@@ -9,17 +9,7 @@ import { act } from 'react';
import { renderHookWithProviders } from '../../test-utils/render.js';
import { createMockSettings } from '../../test-utils/settings.js';
import { waitFor } from '../../test-utils/async.js';
import type { Mock } from 'vitest';
import {
vi,
afterAll,
beforeAll,
describe,
it,
expect,
beforeEach,
afterEach,
} from 'vitest';
import { vi, afterAll, beforeAll, type Mock } from 'vitest';
import {
useKeypressContext,
ESC_TIMEOUT,
@@ -441,80 +431,6 @@ describe('KeypressContext', () => {
);
});
describe('Windows Terminal Backspace handling', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it('should NOT treat \\b as ctrl when WT_SESSION is NOT present and OS is not Windows_NT', async () => {
vi.stubEnv('WT_SESSION', '');
vi.stubEnv('OS', 'Linux');
const { keyHandler } = await setupKeypressTest();
act(() => {
stdin.write('\b');
});
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'backspace',
ctrl: false,
}),
);
});
it('should treat \\b as ctrl when WT_SESSION IS present (even if not Windows_NT)', async () => {
vi.stubEnv('WT_SESSION', 'some-id');
vi.stubEnv('OS', 'Linux');
const { keyHandler } = await setupKeypressTest();
act(() => {
stdin.write('\b');
});
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'backspace',
ctrl: true,
}),
);
});
it('should treat \\b as ctrl when OS is Windows_NT', async () => {
vi.stubEnv('WT_SESSION', '');
vi.stubEnv('OS', 'Windows_NT');
const { keyHandler } = await setupKeypressTest();
act(() => {
stdin.write('\b');
});
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'backspace',
ctrl: true,
}),
);
});
it('should treat \\x7f as regular backspace regardless of WT_SESSION or OS', async () => {
vi.stubEnv('WT_SESSION', 'some-id');
vi.stubEnv('OS', 'Windows_NT');
const { keyHandler } = await setupKeypressTest();
act(() => {
stdin.write('\x7f');
});
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'backspace',
ctrl: false,
}),
);
});
});
describe('paste mode', () => {
it.each([
{
@@ -651,20 +651,8 @@ function* emitKeys(
// tab
name = 'tab';
alt = escaped;
} else if (ch === '\b') {
// ctrl+h / ctrl+backspace (windows terminals send \x08 for ctrl+backspace)
name = 'backspace';
// In Windows environments, \b is sent for Ctrl+Backspace (standard backspace is translated to \x7f).
// We scope this to Windows/WT_SESSION to avoid breaking other unixes where \b is a plain backspace.
if (
typeof process !== 'undefined' &&
(process.env?.['OS'] === 'Windows_NT' || !!process.env?.['WT_SESSION'])
) {
ctrl = true;
}
alt = escaped;
} else if (ch === '\x7f') {
// backspace
} else if (ch === '\b' || ch === '\x7f') {
// backspace or ctrl+h
name = 'backspace';
alt = escaped;
} else if (ch === ESC) {
@@ -513,70 +513,6 @@ describe('useSlashCompletion', () => {
unmountResume();
});
it('should NOT suggest the auto-list command when typing a non-matching partial after /chat', async () => {
const slashCommands = [
createTestCommand({
name: 'chat',
description: 'Manage chat history',
subCommands: [
createTestCommand({ name: 'list', description: 'List chats' }),
],
}),
];
const { result, unmount } = await renderHook(() =>
useTestHarnessForSlashCompletion(
true,
'/chat x', // 'x' does not match 'list'
slashCommands,
mockCommandContext,
),
);
await resolveMatch();
await waitFor(() => {
// It should NOT have the 'auto' section 'list' suggestion
const autoSuggestion = result.current.suggestions.find(
(s) => s.sectionTitle === 'auto',
);
expect(autoSuggestion).toBeUndefined();
});
unmount();
});
it('should STILL suggest the auto-list command when typing a matching partial after /chat', async () => {
const slashCommands = [
createTestCommand({
name: 'chat',
description: 'Manage chat history',
subCommands: [
createTestCommand({ name: 'list', description: 'List chats' }),
],
}),
];
const { result, unmount } = await renderHook(() =>
useTestHarnessForSlashCompletion(
true,
'/chat l', // 'l' matches 'list'
slashCommands,
mockCommandContext,
),
);
await resolveMatch();
await waitFor(() => {
const autoSuggestion = result.current.suggestions.find(
(s) => s.sectionTitle === 'auto',
);
expect(autoSuggestion).toBeDefined();
expect(autoSuggestion?.label).toBe('list');
});
unmount();
});
it('should sort exact altName matches to the top', async () => {
const slashCommands = [
createTestCommand({
+11 -17
View File
@@ -338,23 +338,17 @@ function useCommandSuggestions(
if (isTopLevelChatOrResumeContext) {
const canonicalParentName = leafCommand.name;
const autoLabel = 'list';
if (
partial === '' ||
autoLabel.toLowerCase().startsWith(partial.toLowerCase())
) {
const autoSectionSuggestion: Suggestion = {
label: autoLabel,
value: autoLabel,
insertValue: canonicalParentName,
description: 'Browse auto-saved chats',
commandKind: CommandKind.BUILT_IN,
sectionTitle: 'auto',
submitValue: `/${canonicalParentName}`,
};
setSuggestions([autoSectionSuggestion, ...finalSuggestions]);
return;
}
const autoSectionSuggestion: Suggestion = {
label: 'list',
value: 'list',
insertValue: canonicalParentName,
description: 'Browse auto-saved chats',
commandKind: CommandKind.BUILT_IN,
sectionTitle: 'auto',
submitValue: `/${canonicalParentName}`,
};
setSuggestions([autoSectionSuggestion, ...finalSuggestions]);
return;
}
setSuggestions(finalSuggestions);
@@ -69,7 +69,6 @@ describe('Session Cleanup (Refactored)', () => {
},
getSessionId: () => 'current123',
getDebugMode: () => false,
getExperimentalGemma: () => false,
initialize: async () => {},
...overrides,
} as unknown as Config;
-2
View File
@@ -12,8 +12,6 @@ export {
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
DEFAULT_GEMINI_EMBEDDING_MODEL,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
} from './src/config/models.js';
export {
serializeTerminalToObject,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"description": "Gemini CLI Core",
"license": "Apache-2.0",
"repository": {
@@ -56,7 +56,7 @@ describe('policyCatalog', () => {
it('marks preview transients as sticky retries', () => {
const [previewPolicy] = getModelPolicyChain({ previewEnabled: true });
expect(previewPolicy.model).toBe(PREVIEW_GEMINI_MODEL);
expect(previewPolicy.stateTransitions.transient).toBe('terminal');
expect(previewPolicy.stateTransitions.transient).toBe('sticky_retry');
});
it('applies default actions and state transitions for unspecified kinds', () => {
@@ -50,7 +50,7 @@ export const SILENT_ACTIONS: ModelPolicyActionMap = {
const DEFAULT_STATE: ModelPolicyStateMap = {
terminal: 'terminal',
transient: 'terminal',
transient: 'sticky_retry',
not_found: 'terminal',
unknown: 'terminal',
};
+1 -45
View File
@@ -960,11 +960,8 @@ describe('Server Config (config.ts)', () => {
});
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
await config.getExperimentsAsync();
await vi.waitFor(() => {
expect(config.getModel()).toBe(PREVIEW_GEMINI_FLASH_MODEL);
});
expect(config.getModel()).toBe(PREVIEW_GEMINI_FLASH_MODEL);
});
it('should NOT switch to flash model if user has Pro access and model is auto', async () => {
@@ -3645,47 +3642,6 @@ describe('Config JIT Initialization', () => {
expect(config.isAutoMemoryEnabled()).toBe(true);
});
it('should return true when experimentalGemma is true', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
experimentalGemma: true,
};
config = new Config(params);
expect(config.getExperimentalGemma()).toBe(true);
});
it('should return false when experimentalGemma is false', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
experimentalGemma: false,
};
config = new Config(params);
expect(config.getExperimentalGemma()).toBe(false);
});
it('should return false when experimentalGemma is not provided', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
};
config = new Config(params);
expect(config.getExperimentalGemma()).toBe(false);
});
it('should be independent of experimentalMemoryV2', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
+14 -28
View File
@@ -711,7 +711,6 @@ export interface ConfigParameters {
autoDistillation?: boolean;
experimentalMemoryV2?: boolean;
experimentalAutoMemory?: boolean;
experimentalGemma?: boolean;
experimentalContextManagementConfig?: string;
experimentalAgentHistoryTruncation?: boolean;
experimentalAgentHistoryTruncationThreshold?: number;
@@ -957,7 +956,6 @@ export class Config implements McpContext, AgentLoopContext {
private readonly experimentalJitContext: boolean;
private readonly experimentalMemoryV2: boolean;
private readonly experimentalAutoMemory: boolean;
private readonly experimentalGemma: boolean;
private readonly experimentalContextManagementConfig?: string;
private readonly memoryBoundaryMarkers: readonly string[];
private readonly topicUpdateNarration: boolean;
@@ -1176,7 +1174,6 @@ export class Config implements McpContext, AgentLoopContext {
this.experimentalJitContext = params.experimentalJitContext ?? true;
this.experimentalMemoryV2 = params.experimentalMemoryV2 ?? true;
this.experimentalAutoMemory = params.experimentalAutoMemory ?? false;
this.experimentalGemma = params.experimentalGemma ?? false;
this.experimentalContextManagementConfig =
params.experimentalContextManagementConfig;
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
@@ -1593,12 +1590,8 @@ export class Config implements McpContext, AgentLoopContext {
return undefined;
});
const [experiments] = await Promise.all([
this.experimentsPromise,
quotaPromise.catch((e) => {
debugLogger.error('Failed to fetch user quota', e);
}),
]);
// Fetch experiments and update timeouts before continuing initialization
const experiments = await this.experimentsPromise;
const requestTimeoutMs = this.getRequestTimeoutMs();
if (requestTimeoutMs !== undefined) {
@@ -1608,6 +1601,8 @@ export class Config implements McpContext, AgentLoopContext {
// Initialize BaseLlmClient now that the ContentGenerator and experiments are available
this.baseLlmClient = new BaseLlmClient(this.contentGenerator, this);
await quotaPromise;
const authType = this.contentGeneratorConfig.authType;
if (
authType === AuthType.USE_GEMINI ||
@@ -1628,21 +1623,16 @@ export class Config implements McpContext, AgentLoopContext {
const adminControlsEnabled =
experiments?.flags[ExperimentFlags.ENABLE_ADMIN_CONTROLS]?.boolValue ??
false;
try {
const adminControls = await fetchAdminControls(
codeAssistServer,
this.getRemoteAdminSettings(),
adminControlsEnabled,
(newSettings: AdminControlsSettings) => {
this.setRemoteAdminSettings(newSettings);
coreEvents.emitAdminSettingsChanged();
},
);
this.setRemoteAdminSettings(adminControls);
} catch (e) {
debugLogger.error('Failed to fetch admin controls', e);
}
const adminControls = await fetchAdminControls(
codeAssistServer,
this.getRemoteAdminSettings(),
adminControlsEnabled,
(newSettings: AdminControlsSettings) => {
this.setRemoteAdminSettings(newSettings);
coreEvents.emitAdminSettingsChanged();
},
);
this.setRemoteAdminSettings(adminControls);
if ((await this.getProModelNoAccess()) && isAutoModel(this.model)) {
this.setModel(PREVIEW_GEMINI_FLASH_MODEL);
@@ -2524,10 +2514,6 @@ export class Config implements McpContext, AgentLoopContext {
return this.experimentalAutoMemory;
}
getExperimentalGemma(): boolean {
return this.experimentalGemma;
}
getExperimentalContextManagementConfig(): string | undefined {
return this.experimentalContextManagementConfig;
}
@@ -89,19 +89,6 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
model: 'gemini-2.5-flash-lite',
},
},
'gemma-4-31b-it': {
extends: 'chat-base-3',
modelConfig: {
model: 'gemma-4-31b-it',
},
},
'gemma-4-26b-a4b-it': {
extends: 'chat-base-3',
modelConfig: {
model: 'gemma-4-26b-a4b-it',
},
},
// Bases for the internal model configs.
'gemini-2.5-flash-base': {
extends: 'base',
@@ -330,23 +317,6 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
isVisible: true,
features: { thinking: false, multimodalToolUse: false },
},
'gemma-4-31b-it': {
displayName: 'gemma-4-31b-it',
tier: 'custom',
family: 'gemma-4',
isPreview: false,
isVisible: true,
features: { thinking: true, multimodalToolUse: false },
},
'gemma-4-26b-a4b-it': {
displayName: 'gemma-4-26b-a4b-it',
tier: 'custom',
family: 'gemma-4',
isPreview: false,
isVisible: true,
features: { thinking: true, multimodalToolUse: false },
},
// Aliases
auto: {
tier: 'auto',
@@ -392,13 +362,6 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
},
modelIdResolutions: {
'gemma-4-31b-it': {
default: 'gemma-4-31b-it',
},
'gemma-4-26b-a4b-it': {
default: 'gemma-4-26b-a4b-it',
},
'gemini-3.1-pro-preview': {
default: 'gemini-3.1-pro-preview',
contexts: [
@@ -557,7 +520,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
stateTransitions: {
terminal: 'terminal',
transient: 'terminal',
transient: 'sticky_retry',
not_found: 'terminal',
unknown: 'terminal',
},
@@ -573,7 +536,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
stateTransitions: {
terminal: 'terminal',
transient: 'terminal',
transient: 'sticky_retry',
not_found: 'terminal',
unknown: 'terminal',
},
@@ -590,7 +553,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
stateTransitions: {
terminal: 'terminal',
transient: 'terminal',
transient: 'sticky_retry',
not_found: 'terminal',
unknown: 'terminal',
},
@@ -606,7 +569,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
stateTransitions: {
terminal: 'terminal',
transient: 'terminal',
transient: 'sticky_retry',
not_found: 'terminal',
unknown: 'terminal',
},
@@ -623,7 +586,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
stateTransitions: {
terminal: 'terminal',
transient: 'terminal',
transient: 'sticky_retry',
not_found: 'terminal',
unknown: 'terminal',
},
@@ -638,7 +601,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
stateTransitions: {
terminal: 'terminal',
transient: 'terminal',
transient: 'sticky_retry',
not_found: 'terminal',
unknown: 'terminal',
},
@@ -654,7 +617,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
},
stateTransitions: {
terminal: 'terminal',
transient: 'terminal',
transient: 'sticky_retry',
not_found: 'terminal',
unknown: 'terminal',
},
-17
View File
@@ -32,8 +32,6 @@ import {
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
isPreviewModel,
isProModel,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
} from './models.js';
import type { Config } from './config.js';
import { ModelConfigService } from '../services/modelConfigService.js';
@@ -358,10 +356,6 @@ describe('getDisplayString', () => {
it('should return the model name as is for other models', () => {
expect(getDisplayString('custom-model')).toBe('custom-model');
expect(getDisplayString(GEMMA_4_31B_IT_MODEL)).toBe(GEMMA_4_31B_IT_MODEL);
expect(getDisplayString(GEMMA_4_26B_A4B_IT_MODEL)).toBe(
GEMMA_4_26B_A4B_IT_MODEL,
);
expect(getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL)).toBe(
DEFAULT_GEMINI_FLASH_LITE_MODEL,
);
@@ -579,17 +573,6 @@ describe('isActiveModel', () => {
expect(isActiveModel(DEFAULT_GEMINI_FLASH_MODEL)).toBe(true);
});
it('should return true for Gemma 4 models only when experimentalGemma is true', () => {
expect(isActiveModel(GEMMA_4_31B_IT_MODEL)).toBe(false);
expect(isActiveModel(GEMMA_4_26B_A4B_IT_MODEL)).toBe(false);
expect(isActiveModel(GEMMA_4_31B_IT_MODEL, false, false, false, true)).toBe(
true,
);
expect(
isActiveModel(GEMMA_4_26B_A4B_IT_MODEL, false, false, false, true),
).toBe(true);
});
it('should return false for Gemini 3.1 models when Gemini 3.1 is not launched', () => {
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL)).toBe(false);
expect(isActiveModel(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL)).toBe(false);
-14
View File
@@ -61,9 +61,6 @@ export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
export const GEMMA_4_31B_IT_MODEL = 'gemma-4-31b-it';
export const GEMMA_4_26B_A4B_IT_MODEL = 'gemma-4-26b-a4b-it';
export const VALID_GEMINI_MODELS = new Set([
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_MODEL,
@@ -73,9 +70,6 @@ export const VALID_GEMINI_MODELS = new Set([
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
]);
export const PREVIEW_GEMINI_MODEL_AUTO = 'auto-gemini-3';
@@ -263,10 +257,6 @@ export function getDisplayString(
return 'Auto (Gemini 3)';
case DEFAULT_GEMINI_MODEL_AUTO:
return 'Auto (Gemini 2.5)';
case GEMMA_4_31B_IT_MODEL:
return GEMMA_4_31B_IT_MODEL;
case GEMMA_4_26B_A4B_IT_MODEL:
return GEMMA_4_26B_A4B_IT_MODEL;
case GEMINI_MODEL_ALIAS_PRO:
return PREVIEW_GEMINI_MODEL;
case GEMINI_MODEL_ALIAS_FLASH:
@@ -448,14 +438,10 @@ export function isActiveModel(
useGemini3_1: boolean = false,
useGemini3_1FlashLite: boolean = false,
useCustomToolModel: boolean = false,
experimentalGemma: boolean = false,
): boolean {
if (!VALID_GEMINI_MODELS.has(model)) {
return false;
}
if (model === GEMMA_4_31B_IT_MODEL || model === GEMMA_4_26B_A4B_IT_MODEL) {
return experimentalGemma;
}
if (model === PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL) {
return useGemini3_1FlashLite;
}
+4
View File
@@ -1244,6 +1244,9 @@ ${JSON.stringify(
count: 2,
});
const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
// Act
const stream = client.sendMessageStream(
[{ text: 'Hi' }],
new AbortController().signal,
@@ -1264,6 +1267,7 @@ ${JSON.stringify(
// Assert
expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
expect(abortSpy).toHaveBeenCalled();
expect(finalResult).toBeInstanceOf(Turn);
});
+10 -1
View File
@@ -684,6 +684,9 @@ export class GeminiClient {
// Re-initialize turn with fresh history
turn = new Turn(this.getChat(), prompt_id);
const controller = new AbortController();
const linkedSignal = AbortSignal.any([signal, controller.signal]);
const loopResult = await this.loopDetector.turnStarted(signal);
if (loopResult.count > 1) {
yield { type: GeminiEventType.LoopDetected };
@@ -744,7 +747,7 @@ export class GeminiClient {
const resultStream = turn.run(
modelConfigKey,
request,
signal,
linkedSignal,
displayContent,
);
let isError = false;
@@ -780,6 +783,7 @@ export class GeminiClient {
}
if (loopDetectedAbort) {
controller.abort();
return turn;
}
@@ -791,8 +795,10 @@ export class GeminiClient {
boundedTurns,
isInvalidStreamRetry,
displayContent,
controller,
);
}
if (isError) {
return turn;
}
@@ -1246,7 +1252,10 @@ export class GeminiClient {
boundedTurns: number,
isInvalidStreamRetry: boolean,
displayContent?: PartListUnion,
controllerToAbort?: AbortController,
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
controllerToAbort?.abort();
// Clear the detection flag so the recursive turn can proceed, but the count remains 1.
this.loopDetector.clearDetection();
-6
View File
@@ -10,23 +10,17 @@ import {
DEFAULT_GEMINI_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
PREVIEW_GEMINI_MODEL,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
} from '../config/models.js';
type Model = string;
type TokenCount = number;
export const DEFAULT_TOKEN_LIMIT = 1_048_576;
export const GEMMA_4_TOKEN_LIMIT = 256_000;
export function tokenLimit(model: Model): TokenCount {
// Add other models as they become relevant or if specified by config
// Pulled from https://ai.google.dev/gemini-api/docs/models
switch (model) {
case GEMMA_4_31B_IT_MODEL:
case GEMMA_4_26B_A4B_IT_MODEL:
return GEMMA_4_TOKEN_LIMIT;
case PREVIEW_GEMINI_MODEL:
case PREVIEW_GEMINI_FLASH_MODEL:
case DEFAULT_GEMINI_MODEL:
@@ -97,30 +97,6 @@
"topK": 64
}
},
"gemma-4-31b-it": {
"model": "gemma-4-31b-it",
"generateContentConfig": {
"temperature": 1,
"topP": 0.95,
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "HIGH"
},
"topK": 64
}
},
"gemma-4-26b-a4b-it": {
"model": "gemma-4-26b-a4b-it",
"generateContentConfig": {
"temperature": 1,
"topP": 0.95,
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "HIGH"
},
"topK": 64
}
},
"gemini-2.5-flash-base": {
"model": "gemini-2.5-flash",
"generateContentConfig": {
@@ -97,30 +97,6 @@
"topK": 64
}
},
"gemma-4-31b-it": {
"model": "gemma-4-31b-it",
"generateContentConfig": {
"temperature": 1,
"topP": 0.95,
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "HIGH"
},
"topK": 64
}
},
"gemma-4-26b-a4b-it": {
"model": "gemma-4-26b-a4b-it",
"generateContentConfig": {
"temperature": 1,
"topP": 0.95,
"thinkingConfig": {
"includeThoughts": true,
"thinkingLevel": "HIGH"
},
"topK": 64
}
},
"gemini-2.5-flash-base": {
"model": "gemini-2.5-flash",
"generateContentConfig": {
@@ -1993,27 +1993,6 @@ describe('mcp-client', () => {
});
});
it('should unconditionally attach a data listener to stderr to prevent process hang', async () => {
const mockStderr = {
on: vi.fn(),
};
vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({
stderr: mockStderr,
} as unknown as SdkClientStdioLib.StdioClientTransport);
await createTransport(
'test-server',
{
command: 'test-command',
},
false, // debugMode = false
MOCK_CONTEXT,
);
expect(mockStderr.on).toHaveBeenCalledWith('data', expect.any(Function));
});
it('sets an env variable GEMINI_CLI=1 for stdio MCP servers', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
+18 -27
View File
@@ -2291,40 +2291,31 @@ export async function createTransport(
transport = new XcodeMcpBridgeFixTransport(transport);
}
// The `XcodeMcpBridgeFixTransport` wrapper hides the underlying `StdioClientTransport`,
// which exposes `stderr` for debug logging. We need to unwrap it to attach the listener.
if (debugMode) {
// The `XcodeMcpBridgeFixTransport` wrapper hides the underlying `StdioClientTransport`,
// which exposes `stderr` for debug logging. We need to unwrap it to attach the listener.
const underlyingTransport =
transport instanceof XcodeMcpBridgeFixTransport
? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(transport as unknown as { transport: unknown }).transport
: transport;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const underlyingTransport =
transport instanceof XcodeMcpBridgeFixTransport
? // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
(transport as any).transport
: transport;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const streamTransport = underlyingTransport as {
stderr?: {
on: (event: string, listener: (data: unknown) => void) => void;
};
};
if (
streamTransport &&
typeof streamTransport === 'object' &&
'stderr' in streamTransport &&
streamTransport.stderr &&
typeof streamTransport.stderr.on === 'function'
) {
streamTransport.stderr.on('data', (data: unknown) => {
if (debugMode) {
const stderrStr = String(data).trim();
if (
underlyingTransport instanceof StdioClientTransport &&
underlyingTransport.stderr
) {
underlyingTransport.stderr.on('data', (data) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const stderrStr = data.toString().trim();
debugLogger.debug(
`[DEBUG] [MCP STDERR (${mcpServerName})]: `,
stderrStr,
);
}
});
});
}
}
return transport;
}
+2 -9
View File
@@ -37,7 +37,6 @@ describe('Tracker Tools Integration', () => {
model: 'gemini-3-flash',
debugMode: false,
});
await config.initialize();
messageBus = new MessageBus(null as unknown as PolicyEngine, false);
});
@@ -121,14 +120,8 @@ describe('Tracker Tools Integration', () => {
);
const tasks = await config.getTrackerService().listTasks();
const parentTask = tasks.find((t) => t.title === 'Parent Task');
const childTask = tasks.find((t) => t.title === 'Child Task');
expect(parentTask).toBeDefined();
expect(childTask).toBeDefined();
const parentId = parentTask!.id;
const childId = childTask!.id;
const parentId = tasks.find((t) => t.title === 'Parent Task')!.id;
const childId = tasks.find((t) => t.title === 'Child Task')!.id;
// Add Dependency
const addDepTool = new TrackerAddDependencyTool(config, messageBus);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-devtools",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"license": "Apache-2.0",
"type": "module",
"main": "dist/src/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-sdk",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"description": "Gemini CLI SDK",
"license": "Apache-2.0",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
-1
View File
@@ -680,7 +680,6 @@ export class TestRig {
key !== 'GEMINI_DEBUG' &&
key !== 'GEMINI_CLI_TEST_VAR' &&
key !== 'GEMINI_CLI_INTEGRATION_TEST' &&
key !== 'GOOGLE_GEMINI_BASE_URL' &&
!key.startsWith('GEMINI_CLI_ACTIVITY_LOG')
) {
delete cleanEnv[key];
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
"version": "0.41.0-nightly.20260423.gaa05b4583",
"version": "0.40.0",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {
File diff suppressed because one or more lines are too long
-51
View File
@@ -1,51 +0,0 @@
# Gemini CLI Bot (Cognitive Repository)
This directory contains the foundational architecture for the `gemini-cli-bot`,
transforming the repository into a proactive, evolutionary system.
It implements a dual-layer approach to balance immediate responsiveness with
long-term strategic optimization.
## Layered Execution Model
### 1. System 1: The Pulse (Reflex Layer)
- **Purpose**: High-frequency, deterministic maintenance and data collection.
- **Frequency**: 30-minute cron (`.github/workflows/gemini-cli-bot-pulse.yml`).
- **Implementation**: Pure TypeScript/JavaScript scripts.
- **Role**: Currently focuses on gathering repository metrics
(`tools/gemini-cli-bot/metrics/scripts`).
- **Output**: Action execution and `metrics-before.csv` artifact generation.
### 2. System 2: The Brain (Reasoning Layer)
- **Purpose**: Strategic investigation, policy refinement, and
self-optimization.
- **Frequency**: 24-hour cron (`.github/workflows/gemini-cli-bot-brain.yml`).
- **Implementation**: Agentic Gemini CLI phases.
- **Role**: Analyzing metric trends and running deeper repository health
investigations.
## Directory Structure
- `metrics/`: Contains the deterministic runner (`index.ts`) and individual
TypeScript scripts (`scripts/`) that use the GitHub CLI to track metrics like
open issues, PR latency, throughput, and reviewer domain expertise.
- `processes/scripts/`: Placeholder directory for future deterministic triage
and routing scripts.
- `investigations/`: Placeholder directory for agentic root-cause analysis
phases.
- `critique/`: Placeholder directory for policy evaluation.
- `history/`: Storage for downloaded metrics artifacts from previous runs.
## Usage
To manually collect repository metrics locally, run the following command from
the workspace root:
```bash
npm run metrics
```
This will execute all scripts within `metrics/scripts/` and output the results
to a `metrics-before.csv` file in the root directory.
-69
View File
@@ -1,69 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { readdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { execSync } from 'node:child_process';
const SCRIPTS_DIR = join(
process.cwd(),
'tools',
'gemini-cli-bot',
'metrics',
'scripts',
);
const OUTPUT_FILE = join(process.cwd(), 'metrics-before.csv');
function processOutputLine(line: string, results: string[]) {
const trimmedLine = line.trim();
if (!trimmedLine) return;
try {
const parsed = JSON.parse(trimmedLine);
if (
parsed &&
typeof parsed === 'object' &&
'metric' in parsed &&
'value' in parsed
) {
results.push(`${parsed.metric},${parsed.value}`);
} else {
results.push(trimmedLine);
}
} catch {
results.push(trimmedLine);
}
}
async function run() {
const scripts = readdirSync(SCRIPTS_DIR).filter(
(file) => file.endsWith('.ts') || file.endsWith('.js'),
);
const results: string[] = ['metric,value'];
for (const script of scripts) {
console.log(`Running metric script: ${script}`);
try {
const scriptPath = join(SCRIPTS_DIR, script);
const output = execSync(`npx tsx ${JSON.stringify(scriptPath)}`, {
encoding: 'utf-8',
});
const lines = output.trim().split('\n');
for (const line of lines) {
processOutputLine(line, results);
}
} catch (error) {
console.error(`Error running ${script}:`, error);
}
}
writeFileSync(OUTPUT_FILE, results.join('\n'));
console.log(`Saved metrics to ${OUTPUT_FILE}`);
}
run().catch(console.error);
@@ -1,157 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* @license
*/
import { GITHUB_OWNER, GITHUB_REPO, MetricOutput } from '../types.js';
import { execSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '../../../../');
try {
// 1. Fetch recent PR numbers and reviews from GitHub (so we have reviewer names/logins)
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100, states: MERGED) {
nodes {
number
reviews(first: 20) {
nodes {
authorAssociation
author { login, ... on User { name } }
}
}
}
}
}
}
`;
const output = execSync(
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] },
);
const data = JSON.parse(output).data.repository;
// 2. Map PR numbers to local commits using git log
const logOutput = execSync('git log -n 5000 --format="%H|%s"', {
cwd: repoRoot,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
const prCommits = new Map<number, string>();
for (const line of logOutput.split('\n')) {
if (!line) continue;
const [hash, subject] = line.split('|');
const match = subject.match(/\(#(\d+)\)$/);
if (match) {
prCommits.set(parseInt(match[1], 10), hash);
}
}
let totalMaintainerReviews = 0;
let maintainerReviewsWithExpertise = 0;
for (const pr of data.pullRequests.nodes) {
if (!pr.reviews?.nodes || pr.reviews.nodes.length === 0) continue;
const commitHash = prCommits.get(pr.number);
if (!commitHash) continue; // Skip if we don't have the commit locally
// 3. Get exact files changed using local git diff-tree, bypassing GraphQL limits
const diffTreeOutput = execSync(
`git diff-tree --no-commit-id --name-only -r ${commitHash}`,
{ cwd: repoRoot, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] },
);
const files = diffTreeOutput.split('\n').filter(Boolean);
if (files.length === 0) continue;
// Cache git log authors per path to avoid redundant child_process calls
const authorCache = new Map<string, string>();
const getAuthors = (targetPath: string) => {
if (authorCache.has(targetPath)) return authorCache.get(targetPath)!;
try {
const authors = execSync(
`git log --format="%an|%ae" -- ${JSON.stringify(targetPath)}`,
{
cwd: repoRoot,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
},
).toLowerCase();
authorCache.set(targetPath, authors);
return authors;
} catch {
authorCache.set(targetPath, '');
return '';
}
};
const reviewersOnPR = new Map<string, { name?: string }>();
for (const review of pr.reviews.nodes) {
if (
['MEMBER', 'OWNER'].includes(review.authorAssociation) &&
review.author?.login
) {
const login = review.author.login.toLowerCase();
if (login.endsWith('[bot]') || login.includes('bot')) continue;
reviewersOnPR.set(login, review.author);
}
}
for (const [login, authorInfo] of reviewersOnPR.entries()) {
totalMaintainerReviews++;
let hasExpertise = false;
const name = authorInfo.name ? authorInfo.name.toLowerCase() : '';
for (const file of files) {
// Precise check: immediate file
let authorsStr = getAuthors(file);
if (authorsStr.includes(login) || (name && authorsStr.includes(name))) {
hasExpertise = true;
break;
}
// Fallback: file's directory
const dir = path.dirname(file);
authorsStr = getAuthors(dir);
if (authorsStr.includes(login) || (name && authorsStr.includes(name))) {
hasExpertise = true;
break;
}
}
if (hasExpertise) {
maintainerReviewsWithExpertise++;
}
}
}
const ratio =
totalMaintainerReviews > 0
? maintainerReviewsWithExpertise / totalMaintainerReviews
: 0;
const timestamp = new Date().toISOString();
process.stdout.write(
JSON.stringify(<MetricOutput>{
metric: 'domain_expertise',
value: Math.round(ratio * 100) / 100,
timestamp,
details: {
totalMaintainerReviews,
maintainerReviewsWithExpertise,
},
}) + '\n',
);
} catch (err) {
process.stderr.write(err instanceof Error ? err.message : String(err));
process.exit(1);
}
@@ -1,138 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* @license
*/
import { GITHUB_OWNER, GITHUB_REPO, MetricOutput } from '../types.js';
import { execSync } from 'node:child_process';
try {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100, states: MERGED) {
nodes {
authorAssociation
createdAt
mergedAt
}
}
issues(last: 100, states: CLOSED) {
nodes {
authorAssociation
createdAt
closedAt
}
}
}
}
`;
const output = execSync(
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data.repository;
const prs = data.pullRequests.nodes.map(
(p: {
authorAssociation: string;
mergedAt: string;
createdAt: string;
}) => ({
association: p.authorAssociation,
latencyHours:
(new Date(p.mergedAt).getTime() - new Date(p.createdAt).getTime()) /
(1000 * 60 * 60),
}),
);
const issues = data.issues.nodes.map(
(i: {
authorAssociation: string;
closedAt: string;
createdAt: string;
}) => ({
association: i.authorAssociation,
latencyHours:
(new Date(i.closedAt).getTime() - new Date(i.createdAt).getTime()) /
(1000 * 60 * 60),
}),
);
const isMaintainer = (assoc: string) =>
['MEMBER', 'OWNER', 'COLLABORATOR'].includes(assoc);
const calculateAvg = (
items: { association: string; latencyHours: number }[],
) =>
items.length
? items.reduce((a, b) => a + b.latencyHours, 0) / items.length
: 0;
const prMaintainers = calculateAvg(
prs.filter((i: { association: string; latencyHours: number }) =>
isMaintainer(i.association),
),
);
const prCommunity = calculateAvg(
prs.filter(
(i: { association: string; latencyHours: number }) =>
!isMaintainer(i.association),
),
);
const prOverall = calculateAvg(prs);
const issueMaintainers = calculateAvg(
issues.filter((i: { association: string; latencyHours: number }) =>
isMaintainer(i.association),
),
);
const issueCommunity = calculateAvg(
issues.filter(
(i: { association: string; latencyHours: number }) =>
!isMaintainer(i.association),
),
);
const issueOverall = calculateAvg(issues);
const timestamp = new Date().toISOString();
const metrics: MetricOutput[] = [
{
metric: 'latency_pr_overall_hours',
value: Math.round(prOverall * 100) / 100,
timestamp,
},
{
metric: 'latency_pr_maintainers_hours',
value: Math.round(prMaintainers * 100) / 100,
timestamp,
},
{
metric: 'latency_pr_community_hours',
value: Math.round(prCommunity * 100) / 100,
timestamp,
},
{
metric: 'latency_issue_overall_hours',
value: Math.round(issueOverall * 100) / 100,
timestamp,
},
{
metric: 'latency_issue_maintainers_hours',
value: Math.round(issueMaintainers * 100) / 100,
timestamp,
},
{
metric: 'latency_issue_community_hours',
value: Math.round(issueCommunity * 100) / 100,
timestamp,
},
];
metrics.forEach((m) => process.stdout.write(JSON.stringify(m) + '\n'));
} catch (err) {
process.stderr.write(err instanceof Error ? err.message : String(err));
process.exit(1);
}
@@ -1,20 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'node:child_process';
try {
const count = execSync(
'gh issue list --state open --limit 1000 --json number --jq length',
{
encoding: 'utf-8',
},
).trim();
console.log(`open_issues,${count}`);
} catch {
// Fallback if gh fails or no issues found
console.log('open_issues,0');
}
@@ -1,20 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'node:child_process';
try {
const count = execSync(
'gh pr list --state open --limit 1000 --json number --jq length',
{
encoding: 'utf-8',
},
).trim();
console.log(`open_prs,${count}`);
} catch {
// Fallback if gh fails or no PRs found
console.log('open_prs,0');
}
@@ -1,82 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* @license
*/
import { GITHUB_OWNER, GITHUB_REPO, MetricOutput } from '../types.js';
import { execSync } from 'node:child_process';
try {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100) {
nodes {
reviews(first: 50) {
nodes {
author { login }
authorAssociation
}
}
}
}
}
}
`;
const output = execSync(
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data.repository;
const reviewCounts: Record<string, number> = {};
for (const pr of data.pullRequests.nodes) {
if (!pr.reviews?.nodes) continue;
// We only count one review per author per PR to avoid counting multiple review comments as multiple reviews
const reviewersOnPR = new Set<string>();
for (const review of pr.reviews.nodes) {
if (
['MEMBER', 'OWNER'].includes(review.authorAssociation) &&
review.author?.login
) {
const login = review.author.login.toLowerCase();
if (login.endsWith('[bot]') || login.includes('bot')) {
continue; // Ignore bots
}
reviewersOnPR.add(review.author.login);
}
}
for (const reviewer of reviewersOnPR) {
reviewCounts[reviewer] = (reviewCounts[reviewer] || 0) + 1;
}
}
const counts = Object.values(reviewCounts);
let variance = 0;
if (counts.length > 0) {
const mean = counts.reduce((a, b) => a + b, 0) / counts.length;
variance =
counts.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / counts.length;
}
const timestamp = new Date().toISOString();
process.stdout.write(
JSON.stringify(<MetricOutput>{
metric: 'review_distribution_variance',
value: Math.round(variance * 100) / 100,
timestamp,
details: reviewCounts,
}) + '\n',
);
} catch (err) {
process.stderr.write(err instanceof Error ? err.message : String(err));
process.exit(1);
}
@@ -1,148 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* @license
*/
import { GITHUB_OWNER, GITHUB_REPO, MetricOutput } from '../types.js';
import { execSync } from 'node:child_process';
try {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100, states: MERGED) {
nodes {
authorAssociation
mergedAt
}
}
issues(last: 100, states: CLOSED) {
nodes {
authorAssociation
closedAt
}
}
}
}
`;
const output = execSync(
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data.repository;
const prs = data.pullRequests.nodes
.map((p: { authorAssociation: string; mergedAt: string }) => ({
association: p.authorAssociation,
date: new Date(p.mergedAt).getTime(),
}))
.sort((a: { date: number }, b: { date: number }) => a.date - b.date);
const issues = data.issues.nodes
.map((i: { authorAssociation: string; closedAt: string }) => ({
association: i.authorAssociation,
date: new Date(i.closedAt).getTime(),
}))
.sort((a: { date: number }, b: { date: number }) => a.date - b.date);
const isMaintainer = (assoc: string) =>
['MEMBER', 'OWNER', 'COLLABORATOR'].includes(assoc);
const calculateThroughput = (
items: { association: string; date: number }[],
) => {
if (items.length < 2) return 0;
const first = items[0].date;
const last = items[items.length - 1].date;
const days = (last - first) / (1000 * 60 * 60 * 24);
return days > 0 ? items.length / days : items.length; // items per day
};
const prOverall = calculateThroughput(prs);
const prMaintainers = calculateThroughput(
prs.filter((i: { association: string; date: number }) =>
isMaintainer(i.association),
),
);
const prCommunity = calculateThroughput(
prs.filter(
(i: { association: string; date: number }) =>
!isMaintainer(i.association),
),
);
const issueOverall = calculateThroughput(issues);
const issueMaintainers = calculateThroughput(
issues.filter((i: { association: string; date: number }) =>
isMaintainer(i.association),
),
);
const issueCommunity = calculateThroughput(
issues.filter(
(i: { association: string; date: number }) =>
!isMaintainer(i.association),
),
);
const timestamp = new Date().toISOString();
const metrics: MetricOutput[] = [
{
metric: 'throughput_pr_overall_per_day',
value: Math.round(prOverall * 100) / 100,
timestamp,
},
{
metric: 'throughput_pr_maintainers_per_day',
value: Math.round(prMaintainers * 100) / 100,
timestamp,
},
{
metric: 'throughput_pr_community_per_day',
value: Math.round(prCommunity * 100) / 100,
timestamp,
},
{
metric: 'throughput_issue_overall_per_day',
value: Math.round(issueOverall * 100) / 100,
timestamp,
},
{
metric: 'throughput_issue_maintainers_per_day',
value: Math.round(issueMaintainers * 100) / 100,
timestamp,
},
{
metric: 'throughput_issue_community_per_day',
value: Math.round(issueCommunity * 100) / 100,
timestamp,
},
{
metric: 'throughput_issue_overall_days_per_issue',
value: issueOverall > 0 ? Math.round((1 / issueOverall) * 100) / 100 : 0,
timestamp,
},
{
metric: 'throughput_issue_maintainers_days_per_issue',
value:
issueMaintainers > 0
? Math.round((1 / issueMaintainers) * 100) / 100
: 0,
timestamp,
},
{
metric: 'throughput_issue_community_days_per_issue',
value:
issueCommunity > 0 ? Math.round((1 / issueCommunity) * 100) / 100 : 0,
timestamp,
},
];
metrics.forEach((m) => process.stdout.write(JSON.stringify(m) + '\n'));
} catch (err) {
process.stderr.write(err instanceof Error ? err.message : String(err));
process.exit(1);
}
@@ -1,157 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* @license
*/
import { GITHUB_OWNER, GITHUB_REPO, MetricOutput } from '../types.js';
import { execSync } from 'node:child_process';
try {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100) {
nodes {
authorAssociation
author { login }
createdAt
comments(first: 20) {
nodes {
author { login }
createdAt
}
}
reviews(first: 20) {
nodes {
author { login }
createdAt
}
}
}
}
issues(last: 100) {
nodes {
authorAssociation
author { login }
createdAt
comments(first: 20) {
nodes {
author { login }
createdAt
}
}
}
}
}
}
`;
const output = execSync(
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data.repository;
const getFirstResponseTime = (item: {
createdAt: string;
author: { login: string };
comments: { nodes: { createdAt: string; author?: { login: string } }[] };
reviews?: { nodes: { createdAt: string; author?: { login: string } }[] };
}) => {
const authorLogin = item.author?.login;
let earliestResponse: number | null = null;
const checkNodes = (
nodes: { createdAt: string; author?: { login: string } }[],
) => {
for (const node of nodes) {
if (node.author?.login && node.author.login !== authorLogin) {
const login = node.author.login.toLowerCase();
if (login.endsWith('[bot]') || login.includes('bot')) {
continue; // Ignore bots
}
const time = new Date(node.createdAt).getTime();
if (!earliestResponse || time < earliestResponse) {
earliestResponse = time;
}
}
}
};
if (item.comments?.nodes) checkNodes(item.comments.nodes);
if (item.reviews?.nodes) checkNodes(item.reviews.nodes);
if (earliestResponse) {
return (
(earliestResponse - new Date(item.createdAt).getTime()) /
(1000 * 60 * 60)
);
}
return null; // No response yet
};
const processItems = (
items: {
authorAssociation: string;
createdAt: string;
author: { login: string };
comments: {
nodes: { createdAt: string; author?: { login: string } }[];
};
reviews?: {
nodes: { createdAt: string; author?: { login: string } }[];
};
}[],
) => {
return items
.map((item) => ({
association: item.authorAssociation,
ttfr: getFirstResponseTime(item),
}))
.filter((i) => i.ttfr !== null) as {
association: string;
ttfr: number;
}[];
};
const prs = processItems(data.pullRequests.nodes);
const issues = processItems(data.issues.nodes);
const allItems = [...prs, ...issues];
const isMaintainer = (assoc: string) => ['MEMBER', 'OWNER'].includes(assoc);
const is1P = (assoc: string) => ['COLLABORATOR'].includes(assoc);
const calculateAvg = (items: { ttfr: number; association: string }[]) =>
items.length ? items.reduce((a, b) => a + b.ttfr, 0) / items.length : 0;
const maintainers = calculateAvg(
allItems.filter((i) => isMaintainer(i.association)),
);
const firstParty = calculateAvg(allItems.filter((i) => is1P(i.association)));
const overall = calculateAvg(allItems);
const timestamp = new Date().toISOString();
const metrics: MetricOutput[] = [
{
metric: 'time_to_first_response_overall_hours',
value: Math.round(overall * 100) / 100,
timestamp,
},
{
metric: 'time_to_first_response_maintainers_hours',
value: Math.round(maintainers * 100) / 100,
timestamp,
},
{
metric: 'time_to_first_response_1p_hours',
value: Math.round(firstParty * 100) / 100,
timestamp,
},
];
metrics.forEach((m) => process.stdout.write(JSON.stringify(m) + '\n'));
} catch (err) {
process.stderr.write(err instanceof Error ? err.message : String(err));
process.exit(1);
}
@@ -1,100 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* @license
*/
import { GITHUB_OWNER, GITHUB_REPO, type MetricOutput } from '../types.js';
import { execSync } from 'node:child_process';
try {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
pullRequests(last: 100, states: MERGED) {
nodes {
authorAssociation
comments { totalCount }
reviews { totalCount }
}
}
issues(last: 100, states: CLOSED) {
nodes {
authorAssociation
comments { totalCount }
}
}
}
}
`;
const output = execSync(
`gh api graphql -F owner=${GITHUB_OWNER} -F repo=${GITHUB_REPO} -f query='${query}'`,
{ encoding: 'utf-8' },
);
const data = JSON.parse(output).data.repository;
const prs = data.pullRequests.nodes;
const issues = data.issues.nodes;
const allItems = [
...prs.map(
(p: {
authorAssociation: string;
comments: { totalCount: number };
reviews?: { totalCount: number };
}) => ({
association: p.authorAssociation,
touches: p.comments.totalCount + (p.reviews ? p.reviews.totalCount : 0),
}),
),
...issues.map(
(i: { authorAssociation: string; comments: { totalCount: number } }) => ({
association: i.authorAssociation,
touches: i.comments.totalCount,
}),
),
];
const isMaintainer = (assoc: string) =>
['MEMBER', 'OWNER', 'COLLABORATOR'].includes(assoc);
const calculateAvg = (items: { touches: number; association: string }[]) =>
items.length ? items.reduce((a, b) => a + b.touches, 0) / items.length : 0;
const overall = calculateAvg(allItems);
const maintainers = calculateAvg(
allItems.filter((i) => isMaintainer(i.association)),
);
const community = calculateAvg(
allItems.filter((i) => !isMaintainer(i.association)),
);
const timestamp = new Date().toISOString();
process.stdout.write(
JSON.stringify(<MetricOutput>{
metric: 'user_touches_overall',
value: Math.round(overall * 100) / 100,
timestamp,
}) + '\n',
);
process.stdout.write(
JSON.stringify(<MetricOutput>{
metric: 'user_touches_maintainers',
value: Math.round(maintainers * 100) / 100,
timestamp,
}) + '\n',
);
process.stdout.write(
JSON.stringify(<MetricOutput>{
metric: 'user_touches_community',
value: Math.round(community * 100) / 100,
timestamp,
}) + '\n',
);
} catch (err) {
process.stderr.write(err instanceof Error ? err.message : String(err));
process.exit(1);
}
-14
View File
@@ -1,14 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export interface MetricOutput {
metric: string;
value: number | string;
timestamp: string;
details?: Record<string, unknown>;
}
export const GITHUB_OWNER = 'google-gemini';
export const GITHUB_REPO = 'gemini-cli';