Merge branch 'main' into memory_usage3

This commit is contained in:
Spencer
2026-04-15 11:26:52 -04:00
committed by GitHub
29 changed files with 489 additions and 576 deletions
+1 -1
View File
@@ -183,7 +183,7 @@ jobs:
needs:
- 'merge_queue_skipper'
- 'parse_run_context'
runs-on: 'macos-latest'
runs-on: 'macos-latest-large'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
steps:
+1 -1
View File
@@ -224,7 +224,7 @@ jobs:
test_mac:
name: 'Test (Mac) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
runs-on: 'macos-latest'
runs-on: 'macos-latest-large'
needs:
- 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
+1 -1
View File
@@ -77,7 +77,7 @@ jobs:
deflake_e2e_mac:
name: 'E2E Test (macOS)'
runs-on: 'macos-latest'
runs-on: 'macos-latest-large'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
+6 -3
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.37.1
# Latest stable release: v0.37.2
Released: April 09, 2026
Released: April 13, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -26,6 +26,9 @@ npm install -g @google/gemini-cli
## What's Changed
- fix(patch): cherry-pick 9d741ab to release/v0.37.1-pr-24565 to patch version
v0.37.1 and create version 0.37.2 by @gemini-cli-robot in
[#25322](https://github.com/google-gemini/gemini-cli/pull/25322)
- fix(acp): handle all InvalidStreamError types gracefully in prompt
[#24540](https://github.com/google-gemini/gemini-cli/pull/24540)
- feat(acp): add support for /about command
@@ -422,4 +425,4 @@ npm install -g @google/gemini-cli
[#24842](https://github.com/google-gemini/gemini-cli/pull/24842)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.36.0...v0.37.1
https://github.com/google-gemini/gemini-cli/compare/v0.36.0...v0.37.2
+15 -328
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"workspaces": [
"packages/*"
],
@@ -1727,29 +1727,6 @@
"node": ">=8"
}
},
"node_modules/@joshua.litt/get-ripgrep": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@joshua.litt/get-ripgrep/-/get-ripgrep-0.0.3.tgz",
"integrity": "sha512-rycdieAKKqXi2bsM7G2ayDiNk5CAX8ZOzsTQsirfOqUKPef04Xw40BWGGyimaOOuvPgLWYt3tPnLLG3TvPXi5Q==",
"license": "MIT",
"dependencies": {
"@lvce-editor/verror": "^1.6.0",
"execa": "^9.5.2",
"extract-zip": "^2.0.1",
"fs-extra": "^11.3.0",
"got": "^14.4.5",
"path-exists": "^5.0.0",
"xdg-basedir": "^5.1.0"
}
},
"node_modules/@joshua.litt/get-ripgrep/node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1932,12 +1909,6 @@
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
"license": "MIT"
},
"node_modules/@lvce-editor/verror": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@lvce-editor/verror/-/verror-1.7.0.tgz",
"integrity": "sha512-+LGuAEIC2L7pbvkyAQVWM2Go0dAy+UWEui28g07zNtZsCBhm+gusBK8PNwLJLV5Jay+TyUYuwLIbJdjLLzqEBg==",
"license": "MIT"
},
"node_modules/@lydell/node-pty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz",
@@ -3590,18 +3561,6 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sindresorhus/is": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz",
"integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
@@ -3614,18 +3573,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.1"
},
"engines": {
"node": ">=14.16"
}
},
"node_modules/@textlint/ast-node-types": {
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.2.tgz",
@@ -3931,12 +3878,6 @@
"integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==",
"license": "MIT"
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT"
},
"node_modules/@types/http-errors": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
@@ -6008,33 +5949,6 @@
"node": ">=8"
}
},
"node_modules/cacheable-lookup": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
"license": "MIT",
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz",
"integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==",
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "^4.0.4",
"get-stream": "^9.0.1",
"http-cache-semantics": "^4.1.1",
"keyv": "^4.5.4",
"mimic-response": "^4.0.0",
"normalize-url": "^8.0.1",
"responselike": "^3.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -6968,33 +6882,6 @@
}
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -7057,15 +6944,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -8428,9 +8306,9 @@
}
},
"node_modules/execa": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
"integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
"integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
@@ -8950,15 +8828,6 @@
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz",
"integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
@@ -9586,43 +9455,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "14.4.8",
"resolved": "https://registry.npmjs.org/got/-/got-14.4.8.tgz",
"integrity": "sha512-vxwU4HuR0BIl+zcT1LYrgBjM+IJjNElOjCzs0aPgHorQyr/V6H6Y73Sn3r3FOlUffvWD+Q5jtRuGWaXkU8Jbhg==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^7.0.1",
"@szmarczak/http-timer": "^5.0.1",
"cacheable-lookup": "^7.0.0",
"cacheable-request": "^12.0.1",
"decompress-response": "^6.0.0",
"form-data-encoder": "^4.0.2",
"http2-wrapper": "^2.2.1",
"lowercase-keys": "^3.0.0",
"p-cancelable": "^4.0.1",
"responselike": "^3.0.0",
"type-fest": "^4.26.1"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/got/node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -9878,12 +9710,6 @@
"entities": "^4.4.0"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause"
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -9917,19 +9743,6 @@
"node": ">= 14"
}
},
"node_modules/http2-wrapper": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
"integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.2.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -11040,6 +10853,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/json-parse-better-errors": {
@@ -11235,6 +11049,7 @@
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
@@ -11699,18 +11514,6 @@
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
"license": "MIT"
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lowlight": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
@@ -11981,18 +11784,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
@@ -12371,18 +12162,6 @@
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/normalize-url": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz",
"integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-normalize-package-bin": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
@@ -12895,15 +12674,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/p-cancelable": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
"integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==",
"license": "MIT",
"engines": {
"node": ">=14.16"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -13743,18 +13513,6 @@
],
"license": "MIT"
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -14195,12 +13953,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"license": "MIT"
},
"node_modules/resolve-dir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
@@ -14235,21 +13987,6 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/responselike": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
"integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
"license": "MIT",
"dependencies": {
"lowercase-keys": "^3.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
@@ -17644,18 +17381,6 @@
}
}
},
"node_modules/xdg-basedir": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
"integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
@@ -17864,7 +17589,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "^7.16.0",
@@ -17979,7 +17704,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
@@ -18079,44 +17804,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/cli/node_modules/execa": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
"integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"cross-spawn": "^7.0.6",
"figures": "^6.1.0",
"get-stream": "^9.0.0",
"human-signals": "^8.0.1",
"is-plain-obj": "^4.1.0",
"is-stream": "^4.0.1",
"npm-run-path": "^6.0.0",
"pretty-ms": "^9.2.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^4.0.0",
"yoctocolors": "^2.1.1"
},
"engines": {
"node": "^18.19.0 || >=20.5.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"packages/cli/node_modules/is-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/cli/node_modules/string-width": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
@@ -18151,7 +17838,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -18162,7 +17849,6 @@
"@google/genai": "1.30.0",
"@grpc/grpc-js": "^1.14.3",
"@iarna/toml": "^2.2.5",
"@joshua.litt/get-ripgrep": "^0.0.3",
"@modelcontextprotocol/sdk": "^1.23.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.211.0",
@@ -18190,6 +17876,7 @@
"diff": "^8.0.3",
"dotenv": "^17.2.4",
"dotenv-expand": "^12.0.3",
"execa": "^9.6.1",
"fast-levenshtein": "^2.0.6",
"fdir": "^6.4.6",
"fzf": "^0.5.2",
@@ -18418,7 +18105,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -18433,7 +18120,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18450,7 +18137,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18468,7 +18155,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"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.39.0-nightly.20260408.e77b22e63"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.40.0-nightly.20260414.g5b1f7375a"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"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.39.0-nightly.20260408.e77b22e63"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.40.0-nightly.20260414.g5b1f7375a"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"description": "Gemini CLI Core",
"license": "Apache-2.0",
"repository": {
@@ -20,7 +20,8 @@
"typecheck": "tsc --noEmit"
},
"files": [
"dist"
"dist",
"vendor"
],
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -31,7 +32,6 @@
"@google/genai": "1.30.0",
"@grpc/grpc-js": "^1.14.3",
"@iarna/toml": "^2.2.5",
"@joshua.litt/get-ripgrep": "^0.0.3",
"@modelcontextprotocol/sdk": "^1.23.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.211.0",
@@ -59,6 +59,7 @@
"diff": "^8.0.3",
"dotenv": "^17.2.4",
"dotenv-expand": "^12.0.3",
"execa": "^9.6.1",
"fast-levenshtein": "^2.0.6",
"fdir": "^6.4.6",
"fzf": "^0.5.2",
+1
View File
@@ -3552,6 +3552,7 @@ export class Config implements McpContext, AgentLoopContext {
registry.registerTool(new RipGrepTool(this, this.messageBus)),
);
} else {
debugLogger.warn(`Ripgrep is not available. Falling back to GrepTool.`);
logRipgrepFallback(this, new RipgrepFallbackEvent(errorString));
maybeRegister(GrepTool, () =>
registry.registerTool(new GrepTool(this, this.messageBus)),
@@ -519,4 +519,70 @@ describe('GeminiChat Network Retries', () => {
}),
);
});
it('should retry on OpenSSL 3.x SSL error during stream iteration (ERR_SSL_SSL/TLS_ALERT_BAD_RECORD_MAC)', async () => {
// OpenSSL 3.x produces a different error code format than OpenSSL 1.x
const sslError = new Error(
'request to https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent failed',
) as NodeJS.ErrnoException & { type?: string };
sslError.type = 'system';
sslError.errno =
'ERR_SSL_SSL/TLS_ALERT_BAD_RECORD_MAC' as unknown as number;
sslError.code = 'ERR_SSL_SSL/TLS_ALERT_BAD_RECORD_MAC';
vi.mocked(mockContentGenerator.generateContentStream)
.mockImplementationOnce(async () =>
(async function* () {
yield {
candidates: [
{ content: { parts: [{ text: 'Partial response...' }] } },
],
} as unknown as GenerateContentResponse;
throw sslError;
})(),
)
.mockImplementationOnce(async () =>
(async function* () {
yield {
candidates: [
{
content: { parts: [{ text: 'Complete response after retry' }] },
finishReason: 'STOP',
},
],
} as unknown as GenerateContentResponse;
})(),
);
const stream = await chat.sendMessageStream(
{ model: 'test-model' },
'test message',
'prompt-id-ssl3-mid-stream',
new AbortController().signal,
LlmRole.MAIN,
);
const events: StreamEvent[] = [];
for await (const event of stream) {
events.push(event);
}
const retryEvent = events.find((e) => e.type === StreamEventType.RETRY);
expect(retryEvent).toBeDefined();
const successChunk = events.find(
(e) =>
e.type === StreamEventType.CHUNK &&
e.value.candidates?.[0]?.content?.parts?.[0]?.text ===
'Complete response after retry',
);
expect(successChunk).toBeDefined();
expect(mockLogNetworkRetryAttempt).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
error_type: 'ERR_SSL_SSL/TLS_ALERT_BAD_RECORD_MAC',
}),
);
});
});
@@ -1762,6 +1762,39 @@ describe('PolicyEngine', () => {
});
describe('shell command parsing failure', () => {
it('should return ALLOW in YOLO mode for dangerous commands due to heuristics override', async () => {
// Create an engine with YOLO mode and a sandbox manager that flags a command as dangerous
const rules: PolicyRule[] = [
{
toolName: '*',
decision: PolicyDecision.ALLOW,
priority: 999,
modes: [ApprovalMode.YOLO],
},
];
const mockSandboxManager = new NoopSandboxManager();
mockSandboxManager.isDangerousCommand = vi.fn().mockReturnValue(true);
mockSandboxManager.isKnownSafeCommand = vi.fn().mockReturnValue(false);
engine = new PolicyEngine({
rules,
approvalMode: ApprovalMode.YOLO,
sandboxManager: mockSandboxManager,
});
const result = await engine.check(
{
name: 'run_shell_command',
args: { command: 'powershell echo "dangerous"' },
},
undefined,
);
// Even though the command is flagged as dangerous, YOLO mode should preserve the ALLOW decision
expect(result.decision).toBe(PolicyDecision.ALLOW);
});
it('should return ALLOW in YOLO mode even if shell command parsing fails', async () => {
const { splitCommands } = await import('../utils/shell-utils.js');
const rules: PolicyRule[] = [
@@ -312,6 +312,13 @@ export class PolicyEngine {
const parsedArgs = parsedObjArgs.map(extractStringFromParseEntry);
if (this.sandboxManager.isDangerousCommand(parsedArgs)) {
if (this.approvalMode === ApprovalMode.YOLO) {
debugLogger.debug(
`[PolicyEngine.check] Command evaluated as dangerous, but YOLO mode is active. Preserving decision: ${command}`,
);
return decision;
}
debugLogger.debug(
`[PolicyEngine.check] Command evaluated as dangerous, forcing ASK_USER: ${command}`,
);
+97 -170
View File
@@ -4,20 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
beforeEach,
afterEach,
afterAll,
vi,
} from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
canUseRipgrep,
RipGrepTool,
ensureRgPath,
type RipGrepToolParams,
getRipgrepPath,
} from './ripGrep.js';
import type { GrepResult } from './tools.js';
import path from 'node:path';
@@ -25,18 +18,21 @@ import { isSubpath } from '../utils/paths.js';
import fs from 'node:fs/promises';
import os from 'node:os';
import type { Config } from '../config/config.js';
import { Storage } from '../config/storage.js';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { spawn, type ChildProcess } from 'node:child_process';
import { PassThrough, Readable } from 'node:stream';
import EventEmitter from 'node:events';
import { downloadRipGrep } from '@joshua.litt/get-ripgrep';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
// Mock dependencies for canUseRipgrep
vi.mock('@joshua.litt/get-ripgrep', () => ({
downloadRipGrep: vi.fn(),
}));
import { fileExists } from '../utils/fileUtils.js';
vi.mock('../utils/fileUtils.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../utils/fileUtils.js')>();
return {
...actual,
fileExists: vi.fn(),
};
});
// Mock child_process for ripgrep calls
vi.mock('child_process', () => ({
@@ -44,161 +40,42 @@ vi.mock('child_process', () => ({
}));
const mockSpawn = vi.mocked(spawn);
const downloadRipGrepMock = vi.mocked(downloadRipGrep);
const originalGetGlobalBinDir = Storage.getGlobalBinDir.bind(Storage);
const storageSpy = vi.spyOn(Storage, 'getGlobalBinDir');
function getRipgrepBinaryName() {
return process.platform === 'win32' ? 'rg.exe' : 'rg';
}
describe('canUseRipgrep', () => {
let tempRootDir: string;
let binDir: string;
beforeEach(async () => {
downloadRipGrepMock.mockReset();
downloadRipGrepMock.mockResolvedValue(undefined);
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ripgrep-bin-'));
binDir = path.join(tempRootDir, 'bin');
await fs.mkdir(binDir, { recursive: true });
storageSpy.mockImplementation(() => binDir);
});
afterEach(async () => {
storageSpy.mockImplementation(() => originalGetGlobalBinDir());
await fs.rm(tempRootDir, { recursive: true, force: true });
beforeEach(() => {
vi.mocked(fileExists).mockReset();
});
it('should return true if ripgrep already exists', async () => {
const existingPath = path.join(binDir, getRipgrepBinaryName());
await fs.writeFile(existingPath, '');
vi.mocked(fileExists).mockResolvedValue(true);
const result = await canUseRipgrep();
expect(result).toBe(true);
expect(downloadRipGrepMock).not.toHaveBeenCalled();
});
it('should download ripgrep and return true if it does not exist initially', async () => {
const expectedPath = path.join(binDir, getRipgrepBinaryName());
downloadRipGrepMock.mockImplementation(async () => {
await fs.writeFile(expectedPath, '');
});
it('should return false if file does not exist', async () => {
vi.mocked(fileExists).mockResolvedValue(false);
const result = await canUseRipgrep();
expect(result).toBe(true);
expect(downloadRipGrep).toHaveBeenCalledWith(binDir);
await expect(fs.access(expectedPath)).resolves.toBeUndefined();
});
it('should return false if download fails and file does not exist', async () => {
const result = await canUseRipgrep();
expect(result).toBe(false);
expect(downloadRipGrep).toHaveBeenCalledWith(binDir);
});
it('should propagate errors from downloadRipGrep', async () => {
const error = new Error('Download failed');
downloadRipGrepMock.mockRejectedValue(error);
await expect(canUseRipgrep()).rejects.toThrow(error);
expect(downloadRipGrep).toHaveBeenCalledWith(binDir);
});
it('should only download once when called concurrently', async () => {
const expectedPath = path.join(binDir, getRipgrepBinaryName());
downloadRipGrepMock.mockImplementation(
() =>
new Promise<void>((resolve, reject) => {
setTimeout(() => {
fs.writeFile(expectedPath, '')
.then(() => resolve())
.catch(reject);
}, 0);
}),
);
const firstCall = ensureRgPath();
const secondCall = ensureRgPath();
const [pathOne, pathTwo] = await Promise.all([firstCall, secondCall]);
expect(pathOne).toBe(expectedPath);
expect(pathTwo).toBe(expectedPath);
expect(downloadRipGrepMock).toHaveBeenCalledTimes(1);
await expect(fs.access(expectedPath)).resolves.toBeUndefined();
});
});
describe('ensureRgPath', () => {
let tempRootDir: string;
let binDir: string;
beforeEach(async () => {
downloadRipGrepMock.mockReset();
downloadRipGrepMock.mockResolvedValue(undefined);
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ripgrep-bin-'));
binDir = path.join(tempRootDir, 'bin');
await fs.mkdir(binDir, { recursive: true });
storageSpy.mockImplementation(() => binDir);
});
afterEach(async () => {
storageSpy.mockImplementation(() => originalGetGlobalBinDir());
await fs.rm(tempRootDir, { recursive: true, force: true });
beforeEach(() => {
vi.mocked(fileExists).mockReset();
});
it('should return rg path if ripgrep already exists', async () => {
const existingPath = path.join(binDir, getRipgrepBinaryName());
await fs.writeFile(existingPath, '');
vi.mocked(fileExists).mockResolvedValue(true);
const rgPath = await ensureRgPath();
expect(rgPath).toBe(existingPath);
expect(downloadRipGrep).not.toHaveBeenCalled();
expect(rgPath).toBe(await getRipgrepPath());
});
it('should return rg path if ripgrep is downloaded successfully', async () => {
const expectedPath = path.join(binDir, getRipgrepBinaryName());
downloadRipGrepMock.mockImplementation(async () => {
await fs.writeFile(expectedPath, '');
});
const rgPath = await ensureRgPath();
expect(rgPath).toBe(expectedPath);
expect(downloadRipGrep).toHaveBeenCalledTimes(1);
await expect(fs.access(expectedPath)).resolves.toBeUndefined();
it('should throw an error if ripgrep cannot be used', async () => {
vi.mocked(fileExists).mockResolvedValue(false);
await expect(ensureRgPath()).rejects.toThrow(
/Cannot find bundled ripgrep binary/,
);
});
it('should throw an error if ripgrep cannot be used after download attempt', async () => {
await expect(ensureRgPath()).rejects.toThrow('Cannot use ripgrep.');
expect(downloadRipGrep).toHaveBeenCalledTimes(1);
});
it('should propagate errors from downloadRipGrep', async () => {
const error = new Error('Download failed');
downloadRipGrepMock.mockRejectedValue(error);
await expect(ensureRgPath()).rejects.toThrow(error);
expect(downloadRipGrep).toHaveBeenCalledWith(binDir);
});
it.runIf(process.platform === 'win32')(
'should detect ripgrep when only rg.exe exists on Windows',
async () => {
const expectedRgExePath = path.join(binDir, 'rg.exe');
await fs.writeFile(expectedRgExePath, '');
const rgPath = await ensureRgPath();
expect(rgPath).toBe(expectedRgExePath);
expect(downloadRipGrep).not.toHaveBeenCalled();
await expect(fs.access(expectedRgExePath)).resolves.toBeUndefined();
},
);
});
// Helper function to create mock spawn implementations
@@ -247,9 +124,6 @@ function createMockSpawn(
describe('RipGrepTool', () => {
let tempRootDir: string;
let tempBinRoot: string;
let binDir: string;
let ripgrepBinaryPath: string;
let grepTool: RipGrepTool;
const abortSignal = new AbortController().signal;
@@ -266,19 +140,12 @@ describe('RipGrepTool', () => {
} as unknown as Config;
beforeEach(async () => {
downloadRipGrepMock.mockReset();
downloadRipGrepMock.mockResolvedValue(undefined);
mockSpawn.mockReset();
mockSpawn.mockImplementation(createMockSpawn());
tempBinRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'ripgrep-bin-'));
binDir = path.join(tempBinRoot, 'bin');
await fs.mkdir(binDir, { recursive: true });
const binaryName = process.platform === 'win32' ? 'rg.exe' : 'rg';
ripgrepBinaryPath = path.join(binDir, binaryName);
await fs.writeFile(ripgrepBinaryPath, '');
storageSpy.mockImplementation(() => binDir);
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
vi.mocked(fileExists).mockResolvedValue(true);
mockConfig = {
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
@@ -335,9 +202,7 @@ describe('RipGrepTool', () => {
});
afterEach(async () => {
storageSpy.mockImplementation(() => originalGetGlobalBinDir());
await fs.rm(tempRootDir, { recursive: true, force: true });
await fs.rm(tempBinRoot, { recursive: true, force: true });
});
describe('validateToolParams', () => {
@@ -834,16 +699,16 @@ describe('RipGrepTool', () => {
});
it('should throw an error if ripgrep is not available', async () => {
await fs.rm(ripgrepBinaryPath, { force: true });
downloadRipGrepMock.mockResolvedValue(undefined);
vi.mocked(fileExists).mockResolvedValue(false);
const params: RipGrepToolParams = { pattern: 'world' };
const invocation = grepTool.build(params);
expect(await invocation.execute({ abortSignal })).toStrictEqual({
llmContent: 'Error during grep search operation: Cannot use ripgrep.',
returnDisplay: 'Error: Cannot use ripgrep.',
});
const result = await invocation.execute({ abortSignal });
expect(result.llmContent).toContain('Cannot find bundled ripgrep binary');
// restore the mock for subsequent tests
vi.mocked(fileExists).mockResolvedValue(true);
});
});
@@ -2080,6 +1945,68 @@ describe('RipGrepTool', () => {
});
});
afterAll(() => {
storageSpy.mockRestore();
describe('getRipgrepPath', () => {
afterEach(() => {
vi.restoreAllMocks();
});
describe('OS/Architecture Resolution', () => {
it.each([
{ platform: 'darwin', arch: 'arm64', expectedBin: 'rg-darwin-arm64' },
{ platform: 'darwin', arch: 'x64', expectedBin: 'rg-darwin-x64' },
{ platform: 'linux', arch: 'arm64', expectedBin: 'rg-linux-arm64' },
{ platform: 'linux', arch: 'x64', expectedBin: 'rg-linux-x64' },
{ platform: 'win32', arch: 'x64', expectedBin: 'rg-win32-x64.exe' },
])(
'should map $platform $arch to $expectedBin',
async ({ platform, arch, expectedBin }) => {
vi.spyOn(os, 'platform').mockReturnValue(platform as NodeJS.Platform);
vi.spyOn(os, 'arch').mockReturnValue(arch);
vi.mocked(fileExists).mockImplementation(async (checkPath) =>
checkPath.endsWith(expectedBin),
);
const resolvedPath = await getRipgrepPath();
expect(resolvedPath).not.toBeNull();
expect(resolvedPath?.endsWith(expectedBin)).toBe(true);
},
);
});
describe('Path Fallback Logic', () => {
beforeEach(() => {
vi.spyOn(os, 'platform').mockReturnValue('linux');
vi.spyOn(os, 'arch').mockReturnValue('x64');
});
it('should resolve the SEA (flattened) path first', async () => {
vi.mocked(fileExists).mockImplementation(async (checkPath) =>
checkPath.includes(path.normalize('tools/vendor/ripgrep')),
);
const resolvedPath = await getRipgrepPath();
expect(resolvedPath).not.toBeNull();
expect(resolvedPath).toContain(path.normalize('tools/vendor/ripgrep'));
});
it('should fall back to the Dev path if SEA path is missing', async () => {
vi.mocked(fileExists).mockImplementation(
async (checkPath) =>
checkPath.includes(path.normalize('core/vendor/ripgrep')) &&
!checkPath.includes('tools'),
);
const resolvedPath = await getRipgrepPath();
expect(resolvedPath).not.toBeNull();
expect(resolvedPath).toContain(path.normalize('core/vendor/ripgrep'));
expect(resolvedPath).not.toContain('tools');
});
it('should return null if binary is missing from both paths', async () => {
vi.mocked(fileExists).mockResolvedValue(false);
const resolvedPath = await getRipgrepPath();
expect(resolvedPath).toBeNull();
});
});
});
+29 -54
View File
@@ -8,7 +8,8 @@ import type { MessageBus } from '../confirmation-bus/message-bus.js';
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import { downloadRipGrep } from '@joshua.litt/get-ripgrep';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import {
BaseDeclarativeTool,
BaseToolInvocation,
@@ -22,7 +23,6 @@ import { makeRelative, shortenPath } from '../utils/paths.js';
import { getErrorMessage, isNodeError } from '../utils/errors.js';
import type { Config } from '../config/config.js';
import { fileExists } from '../utils/fileUtils.js';
import { Storage } from '../config/storage.js';
import { GREP_TOOL_NAME } from './tool-names.js';
import { debugLogger } from '../utils/debugLogger.js';
import {
@@ -39,73 +39,48 @@ import { RIP_GREP_DEFINITION } from './definitions/coreTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
import { type GrepMatch, formatGrepResults } from './grep-utils.js';
function getRgCandidateFilenames(): readonly string[] {
return process.platform === 'win32' ? ['rg.exe', 'rg'] : ['rg'];
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function resolveExistingRgPath(): Promise<string | null> {
const binDir = Storage.getGlobalBinDir();
for (const fileName of getRgCandidateFilenames()) {
const candidatePath = path.join(binDir, fileName);
if (await fileExists(candidatePath)) {
return candidatePath;
export async function getRipgrepPath(): Promise<string | null> {
const platform = os.platform();
const arch = os.arch();
// Map to the correct bundled binary
const binName = `rg-${platform}-${arch}${platform === 'win32' ? '.exe' : ''}`;
const candidatePaths = [
// 1. SEA runtime layout: everything is flattened into the root dir
path.resolve(__dirname, 'vendor/ripgrep', binName),
// 2. Dev/Dist layout: packages/core/dist/tools/ripGrep.js -> packages/core/vendor/ripgrep
path.resolve(__dirname, '../../vendor/ripgrep', binName),
];
for (const candidate of candidatePaths) {
if (await fileExists(candidate)) {
return candidate;
}
}
return null;
}
let ripgrepAcquisitionPromise: Promise<string | null> | null = null;
/**
* Ensures a ripgrep binary is available.
*
* NOTE:
* - The Gemini CLI currently prefers a managed ripgrep binary downloaded
* into its global bin directory.
* - Even if ripgrep is available on the system PATH, it is intentionally
* not used at this time.
*
* Preference for system-installed ripgrep is blocked on:
* - checksum verification of external binaries
* - internalization of the get-ripgrep dependency
*
* See:
* - feat(core): Prefer rg in system path (#11847)
* - Move get-ripgrep to third_party (#12099)
*/
async function ensureRipgrepAvailable(): Promise<string | null> {
const existingPath = await resolveExistingRgPath();
if (existingPath) {
return existingPath;
}
if (!ripgrepAcquisitionPromise) {
ripgrepAcquisitionPromise = (async () => {
try {
await downloadRipGrep(Storage.getGlobalBinDir());
return await resolveExistingRgPath();
} finally {
ripgrepAcquisitionPromise = null;
}
})();
}
return ripgrepAcquisitionPromise;
}
/**
* Checks if `rg` exists, if not then attempt to download it.
* Checks if `rg` exists in the bundled vendor directory.
*/
export async function canUseRipgrep(): Promise<boolean> {
return (await ensureRipgrepAvailable()) !== null;
const binPath = await getRipgrepPath();
return binPath !== null;
}
/**
* Ensures `rg` is downloaded, or throws.
* Ensures `rg` is available, or throws.
*/
export async function ensureRgPath(): Promise<string> {
const downloadedPath = await ensureRipgrepAvailable();
if (downloadedPath) {
return downloadedPath;
const binPath = await getRipgrepPath();
if (binPath !== null) {
return binPath;
}
throw new Error('Cannot use ripgrep.');
throw new Error(`Cannot find bundled ripgrep binary.`);
}
/**
+5
View File
@@ -48,6 +48,7 @@ import {
} from '../utils/shell-utils.js';
import { SHELL_TOOL_NAME } from './tool-names.js';
import { PARAM_ADDITIONAL_PERMISSIONS } from './definitions/base-declarations.js';
import { ApprovalMode } from '../policy/types.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { getShellDefinition } from './definitions/coreTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
@@ -252,6 +253,10 @@ export class ShellToolInvocation extends BaseToolInvocation<
abortSignal: AbortSignal,
forcedDecision?: ForcedToolDecision,
): Promise<ToolCallConfirmationDetails | false> {
if (this.context.config.getApprovalMode() === ApprovalMode.YOLO) {
return super.shouldConfirmExecute(abortSignal, forcedDecision);
}
if (this.params[PARAM_ADDITIONAL_PERMISSIONS]) {
return this.getConfirmationDetails(abortSignal);
}
+34
View File
@@ -511,6 +511,40 @@ describe('retryWithBackoff', () => {
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should retry on OpenSSL 3.x SSL error code (ERR_SSL_SSL/TLS_ALERT_BAD_RECORD_MAC)', async () => {
const error = new Error('SSL error');
(error as any).code = 'ERR_SSL_SSL/TLS_ALERT_BAD_RECORD_MAC';
const mockFn = vi
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue('success');
const promise = retryWithBackoff(mockFn, {
initialDelayMs: 1,
maxDelayMs: 1,
});
await vi.runAllTimersAsync();
await expect(promise).resolves.toBe('success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should retry on unknown SSL BAD_RECORD_MAC variant via substring fallback', async () => {
const error = new Error('SSL error');
(error as any).code = 'ERR_SSL_SOME_FUTURE_BAD_RECORD_MAC';
const mockFn = vi
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue('success');
const promise = retryWithBackoff(mockFn, {
initialDelayMs: 1,
maxDelayMs: 1,
});
await vi.runAllTimersAsync();
await expect(promise).resolves.toBe('success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should retry on gaxios-style SSL error with code property', async () => {
// This matches the exact structure from issue #17318
const error = new Error(
+22 -6
View File
@@ -53,14 +53,30 @@ const RETRYABLE_NETWORK_CODES = [
'ENOTFOUND',
'EAI_AGAIN',
'ECONNREFUSED',
// SSL/TLS transient errors
'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC',
'ERR_SSL_WRONG_VERSION_NUMBER',
'ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC',
'ERR_SSL_BAD_RECORD_MAC',
'EPROTO', // Generic protocol error (often SSL-related)
];
// Node.js builds SSL error codes by prepending ERR_SSL_ to the uppercased
// OpenSSL reason string with spaces replaced by underscores (see
// TLSWrap::ClearOut in node/src/crypto/crypto_tls.cc). The reason string
// format varies by OpenSSL version (e.g. ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC
// on OpenSSL 1.x, ERR_SSL_SSL/TLS_ALERT_BAD_RECORD_MAC on OpenSSL 3.x), so
// match the stable suffix instead of enumerating every variant.
const RETRYABLE_SSL_ERROR_PATTERN = /^ERR_SSL_.*BAD_RECORD_MAC/i;
/**
* Returns true if the error code should be retried: either an exact match
* against RETRYABLE_NETWORK_CODES, or an SSL BAD_RECORD_MAC variant (the
* OpenSSL reason-string portion of the code varies across OpenSSL versions).
*/
function isRetryableSslErrorCode(code: string): boolean {
return (
RETRYABLE_NETWORK_CODES.includes(code) ||
RETRYABLE_SSL_ERROR_PATTERN.test(code)
);
}
function getNetworkErrorCode(error: unknown): string | undefined {
const getCode = (obj: unknown): string | undefined => {
if (typeof obj !== 'object' || obj === null) {
@@ -112,7 +128,7 @@ export function getRetryErrorType(error: unknown): string {
}
const errorCode = getNetworkErrorCode(error);
if (errorCode && RETRYABLE_NETWORK_CODES.includes(errorCode)) {
if (errorCode && isRetryableSslErrorCode(errorCode)) {
return errorCode;
}
@@ -153,7 +169,7 @@ export function isRetryableError(
): boolean {
// Check for common network error codes
const errorCode = getNetworkErrorCode(error);
if (errorCode && RETRYABLE_NETWORK_CODES.includes(errorCode)) {
if (errorCode && isRetryableSslErrorCode(errorCode)) {
return true;
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-devtools",
"version": "0.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"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.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"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.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+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.39.0-nightly.20260408.e77b22e63",
"version": "0.40.0-nightly.20260414.g5b1f7375a",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {
+12
View File
@@ -108,4 +108,16 @@ if (!existsSync(bundleMcpSrc)) {
cpSync(bundleMcpSrc, bundleMcpDest, { recursive: true, dereference: true });
console.log('Copied bundled chrome-devtools-mcp to bundle/bundled/');
// 7. Copy pre-built ripgrep vendor binaries
const ripgrepVendorSrc = join(root, 'packages/core/vendor/ripgrep');
const ripgrepVendorDest = join(bundleDir, 'vendor', 'ripgrep');
if (existsSync(ripgrepVendorSrc)) {
mkdirSync(ripgrepVendorDest, { recursive: true });
cpSync(ripgrepVendorSrc, ripgrepVendorDest, {
recursive: true,
dereference: true,
});
console.log('Copied ripgrep vendor binaries to bundle/vendor/ripgrep/');
}
console.log('Assets copied to bundle/');
+146
View File
@@ -0,0 +1,146 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview This script downloads pre-built ripgrep binaries for all supported
* architectures and platforms. These binaries are checked into the repository
* under packages/core/vendor/ripgrep.
*
* Maintainers should periodically run this script to upgrade the version
* of ripgrep being distributed.
*
* Usage: npx tsx scripts/download-ripgrep-binaries.ts
*/
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { fileURLToPath } from 'node:url';
import { createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';
import type { ReadableStream } from 'node:stream/web';
import { execFileSync } from 'node:child_process';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CORE_VENDOR_DIR = path.join(__dirname, '../packages/core/vendor/ripgrep');
const VERSION = 'v13.0.0-10';
interface Target {
platform: string;
arch: string;
file: string;
}
const targets: Target[] = [
{ platform: 'darwin', arch: 'arm64', file: 'aarch64-apple-darwin.tar.gz' },
{ platform: 'darwin', arch: 'x64', file: 'x86_64-apple-darwin.tar.gz' },
{
platform: 'linux',
arch: 'arm64',
file: 'aarch64-unknown-linux-gnu.tar.gz',
},
{ platform: 'linux', arch: 'x64', file: 'x86_64-unknown-linux-musl.tar.gz' },
{ platform: 'win32', arch: 'x64', file: 'x86_64-pc-windows-msvc.zip' },
];
async function downloadBinary() {
await fsPromises.mkdir(CORE_VENDOR_DIR, { recursive: true });
for (const target of targets) {
const url = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/${VERSION}/ripgrep-${VERSION}-${target.file}`;
const archivePath = path.join(CORE_VENDOR_DIR, target.file);
const binName = `rg-${target.platform}-${target.arch}${target.platform === 'win32' ? '.exe' : ''}`;
const finalBinPath = path.join(CORE_VENDOR_DIR, binName);
if (fs.existsSync(finalBinPath)) {
console.log(`[Cache] ${binName} already exists.`);
continue;
}
console.log(`[Download] ${url} -> ${archivePath}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}
if (!response.body) {
throw new Error(`Response body is null for ${url}`);
}
const fileStream = createWriteStream(archivePath);
// Node 18+ global fetch response.body is a ReadableStream (web stream)
// pipeline(Readable.fromWeb(response.body), fileStream) works in Node 18+
await pipeline(
Readable.fromWeb(response.body as ReadableStream),
fileStream,
);
console.log(`[Extract] Extracting ${archivePath}...`);
// Extract using shell commands for simplicity
if (target.file.endsWith('.tar.gz')) {
execFileSync('tar', ['-xzf', archivePath, '-C', CORE_VENDOR_DIR]);
// Microsoft's ripgrep release extracts directly to `rg` inside the current directory sometimes
const sourceBin = path.join(CORE_VENDOR_DIR, 'rg');
if (fs.existsSync(sourceBin)) {
await fsPromises.rename(sourceBin, finalBinPath);
} else {
// Fallback for sub-directory if it happens
const extractedDirName = `ripgrep-${VERSION}-${target.file.replace('.tar.gz', '')}`;
const fallbackSourceBin = path.join(
CORE_VENDOR_DIR,
extractedDirName,
'rg',
);
if (fs.existsSync(fallbackSourceBin)) {
await fsPromises.rename(fallbackSourceBin, finalBinPath);
await fsPromises.rm(path.join(CORE_VENDOR_DIR, extractedDirName), {
recursive: true,
force: true,
});
} else {
throw new Error(
`Could not find extracted 'rg' binary for ${target.platform} ${target.arch}`,
);
}
}
} else if (target.file.endsWith('.zip')) {
execFileSync('unzip', ['-o', '-q', archivePath, '-d', CORE_VENDOR_DIR]);
const sourceBin = path.join(CORE_VENDOR_DIR, 'rg.exe');
if (fs.existsSync(sourceBin)) {
await fsPromises.rename(sourceBin, finalBinPath);
} else {
const extractedDirName = `ripgrep-${VERSION}-${target.file.replace('.zip', '')}`;
const fallbackSourceBin = path.join(
CORE_VENDOR_DIR,
extractedDirName,
'rg.exe',
);
if (fs.existsSync(fallbackSourceBin)) {
await fsPromises.rename(fallbackSourceBin, finalBinPath);
await fsPromises.rm(path.join(CORE_VENDOR_DIR, extractedDirName), {
recursive: true,
force: true,
});
} else {
throw new Error(
`Could not find extracted 'rg.exe' binary for ${target.platform} ${target.arch}`,
);
}
}
}
// Clean up archive
await fsPromises.unlink(archivePath);
console.log(`[Success] Saved to ${finalBinPath}`);
}
}
downloadBinary().catch((err) => {
console.error(err);
process.exit(1);
});