diff --git a/.github/workflows/chained_e2e.yml b/.github/workflows/chained_e2e.yml index 94215e4795..e6385ad4bb 100644 --- a/.github/workflows/chained_e2e.yml +++ b/.github/workflows/chained_e2e.yml @@ -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: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82e9194a02..0bc2cf03ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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'" diff --git a/.github/workflows/deflake.yml b/.github/workflows/deflake.yml index 98635dbda7..cd61346ffa 100644 --- a/.github/workflows/deflake.yml +++ b/.github/workflows/deflake.yml @@ -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' diff --git a/docs/changelogs/latest.md b/docs/changelogs/latest.md index 3184abf79d..bccbc4bd77 100644 --- a/docs/changelogs/latest.md +++ b/docs/changelogs/latest.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 0df4109d84..ab847dedd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 150abcf3c3..1848b1587c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json index 51e0450c97..deddcf53d3 100644 --- a/packages/a2a-server/package.json +++ b/packages/a2a-server/package.json @@ -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", diff --git a/packages/cli/package.json b/packages/cli/package.json index cd3b2ec135..f239816947 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/core/package.json b/packages/core/package.json index 00c663690d..dc18347a04 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index c19cc257c3..a9c0b813ee 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -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)), diff --git a/packages/core/src/core/geminiChat_network_retry.test.ts b/packages/core/src/core/geminiChat_network_retry.test.ts index 4683e29261..83d5848e75 100644 --- a/packages/core/src/core/geminiChat_network_retry.test.ts +++ b/packages/core/src/core/geminiChat_network_retry.test.ts @@ -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', + }), + ); + }); }); diff --git a/packages/core/src/policy/policy-engine.test.ts b/packages/core/src/policy/policy-engine.test.ts index b6c11a079b..5606c49793 100644 --- a/packages/core/src/policy/policy-engine.test.ts +++ b/packages/core/src/policy/policy-engine.test.ts @@ -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[] = [ diff --git a/packages/core/src/policy/policy-engine.ts b/packages/core/src/policy/policy-engine.ts index eb5b141ba5..a9e049c74d 100644 --- a/packages/core/src/policy/policy-engine.ts +++ b/packages/core/src/policy/policy-engine.ts @@ -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}`, ); diff --git a/packages/core/src/tools/ripGrep.test.ts b/packages/core/src/tools/ripGrep.test.ts index 000e3db3e1..9ad575833a 100644 --- a/packages/core/src/tools/ripGrep.test.ts +++ b/packages/core/src/tools/ripGrep.test.ts @@ -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(); + 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((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(); + }); + }); }); diff --git a/packages/core/src/tools/ripGrep.ts b/packages/core/src/tools/ripGrep.ts index 4449a7a08a..c2ae482289 100644 --- a/packages/core/src/tools/ripGrep.ts +++ b/packages/core/src/tools/ripGrep.ts @@ -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 { - 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 { + 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 | 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 { - 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 { - 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 { - 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.`); } /** diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index f3c0f9a571..056df31f2a 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -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 { + if (this.context.config.getApprovalMode() === ApprovalMode.YOLO) { + return super.shouldConfirmExecute(abortSignal, forcedDecision); + } + if (this.params[PARAM_ADDITIONAL_PERMISSIONS]) { return this.getConfirmationDetails(abortSignal); } diff --git a/packages/core/src/utils/retry.test.ts b/packages/core/src/utils/retry.test.ts index a5b5a8b657..29758e6e92 100644 --- a/packages/core/src/utils/retry.test.ts +++ b/packages/core/src/utils/retry.test.ts @@ -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( diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index 46765216b9..5b3ac4f113 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -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; } diff --git a/packages/core/vendor/ripgrep/rg-darwin-arm64 b/packages/core/vendor/ripgrep/rg-darwin-arm64 new file mode 100755 index 0000000000..e163565822 Binary files /dev/null and b/packages/core/vendor/ripgrep/rg-darwin-arm64 differ diff --git a/packages/core/vendor/ripgrep/rg-darwin-x64 b/packages/core/vendor/ripgrep/rg-darwin-x64 new file mode 100755 index 0000000000..ef047368a7 Binary files /dev/null and b/packages/core/vendor/ripgrep/rg-darwin-x64 differ diff --git a/packages/core/vendor/ripgrep/rg-linux-arm64 b/packages/core/vendor/ripgrep/rg-linux-arm64 new file mode 100755 index 0000000000..38c7ec9ae0 Binary files /dev/null and b/packages/core/vendor/ripgrep/rg-linux-arm64 differ diff --git a/packages/core/vendor/ripgrep/rg-linux-x64 b/packages/core/vendor/ripgrep/rg-linux-x64 new file mode 100755 index 0000000000..acf3d8ef76 Binary files /dev/null and b/packages/core/vendor/ripgrep/rg-linux-x64 differ diff --git a/packages/core/vendor/ripgrep/rg-win32-x64.exe b/packages/core/vendor/ripgrep/rg-win32-x64.exe new file mode 100644 index 0000000000..bd0e08ee46 Binary files /dev/null and b/packages/core/vendor/ripgrep/rg-win32-x64.exe differ diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 60eba8c1a6..8d6a9c9f3e 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -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", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 225b60ce2d..574199544a 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -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": { diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 8a1d11000f..5137c6089e 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -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", diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index da5931edd3..b7e8446884 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -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": { diff --git a/scripts/copy_bundle_assets.js b/scripts/copy_bundle_assets.js index ef6a68e58d..667e911f0e 100644 --- a/scripts/copy_bundle_assets.js +++ b/scripts/copy_bundle_assets.js @@ -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/'); diff --git a/scripts/download-ripgrep-binaries.ts b/scripts/download-ripgrep-binaries.ts new file mode 100644 index 0000000000..969d69c7eb --- /dev/null +++ b/scripts/download-ripgrep-binaries.ts @@ -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); +});