mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 05:01:04 -07:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 563c83c4a3 | |||
| d5a135b144 | |||
| f7a4e7281b | |||
| 3d2d44d74d | |||
| 9c7a329cea | |||
| 7f91d7501b | |||
| f38a2ce0cb | |||
| ec10a76923 | |||
| 57c7f6faeb | |||
| 93c2f0cabb | |||
| 7fe17b5dad | |||
| 3384a73b00 | |||
| 88b0b0fe8b | |||
| 8349cc0d10 | |||
| f55b52ab58 | |||
| fd85363d13 | |||
| 7ce9aa518e | |||
| 68bde280d0 | |||
| 09800ea74e | |||
| 9cf06a3e00 | |||
| 3c33fcb69c | |||
| 6f4a2a2c2a | |||
| 9954615542 | |||
| c7a044b091 |
@@ -129,8 +129,9 @@ they appear in the UI.
|
|||||||
|
|
||||||
### HooksConfig
|
### HooksConfig
|
||||||
|
|
||||||
| UI Label | Setting | Description | Default |
|
| UI Label | Setting | Description | Default |
|
||||||
| ------------------ | --------------------------- | ------------------------------------------------ | ------- |
|
| ------------------ | --------------------------- | -------------------------------------------------------------------------------- | ------- |
|
||||||
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
|
| Enable Hooks | `hooksConfig.enabled` | Canonical toggle for the hooks system. When disabled, no hooks will be executed. | `true` |
|
||||||
|
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
|
||||||
|
|
||||||
<!-- SETTINGS-AUTOGEN:END -->
|
<!-- SETTINGS-AUTOGEN:END -->
|
||||||
|
|||||||
@@ -902,6 +902,7 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **Description:** Canonical toggle for the hooks system. When disabled, no
|
- **Description:** Canonical toggle for the hooks system. When disabled, no
|
||||||
hooks will be executed.
|
hooks will be executed.
|
||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
- **`hooksConfig.disabled`** (array):
|
- **`hooksConfig.disabled`** (array):
|
||||||
- **Description:** List of hook names (commands) that should be disabled.
|
- **Description:** List of hook names (commands) that should be disabled.
|
||||||
|
|||||||
Generated
+8
-31
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -2251,7 +2251,6 @@
|
|||||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^6.0.0",
|
"@octokit/auth-token": "^6.0.0",
|
||||||
"@octokit/graphql": "^9.0.2",
|
"@octokit/graphql": "^9.0.2",
|
||||||
@@ -2432,7 +2431,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
@@ -2466,7 +2464,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
},
|
},
|
||||||
@@ -2835,7 +2832,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
@@ -2869,7 +2865,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/resources": "2.0.1"
|
"@opentelemetry/resources": "2.0.1"
|
||||||
@@ -2922,7 +2917,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/resources": "2.0.1",
|
"@opentelemetry/resources": "2.0.1",
|
||||||
@@ -4128,7 +4122,6 @@
|
|||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -4406,7 +4399,6 @@
|
|||||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.35.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.35.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
@@ -5399,7 +5391,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -8409,7 +8400,6 @@
|
|||||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -8950,7 +8940,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
@@ -10552,7 +10541,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
||||||
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
@@ -14311,7 +14299,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -14322,7 +14309,6 @@
|
|||||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shell-quote": "^1.6.1",
|
"shell-quote": "^1.6.1",
|
||||||
"ws": "^7"
|
"ws": "^7"
|
||||||
@@ -16559,7 +16545,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -16783,8 +16768,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD",
|
"license": "0BSD"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.20.3",
|
"version": "4.20.3",
|
||||||
@@ -16792,7 +16776,6 @@
|
|||||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.25.0",
|
"esbuild": "~0.25.0",
|
||||||
"get-tsconfig": "^4.7.5"
|
"get-tsconfig": "^4.7.5"
|
||||||
@@ -16965,7 +16948,6 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -17173,7 +17155,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -17287,7 +17268,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -17300,7 +17280,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
@@ -18005,7 +17984,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -18021,7 +17999,7 @@
|
|||||||
},
|
},
|
||||||
"packages/a2a-server": {
|
"packages/a2a-server": {
|
||||||
"name": "@google/gemini-cli-a2a-server",
|
"name": "@google/gemini-cli-a2a-server",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "^0.3.8",
|
"@a2a-js/sdk": "^0.3.8",
|
||||||
"@google-cloud/storage": "^7.16.0",
|
"@google-cloud/storage": "^7.16.0",
|
||||||
@@ -18077,7 +18055,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "^0.12.0",
|
"@agentclientprotocol/sdk": "^0.12.0",
|
||||||
@@ -18164,7 +18142,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@google/gemini-cli-core",
|
"name": "@google/gemini-cli-core",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "^0.3.8",
|
"@a2a-js/sdk": "^0.3.8",
|
||||||
@@ -18300,7 +18278,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -18323,7 +18300,7 @@
|
|||||||
},
|
},
|
||||||
"packages/test-utils": {
|
"packages/test-utils": {
|
||||||
"name": "@google/gemini-cli-test-utils",
|
"name": "@google/gemini-cli-test-utils",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/gemini-cli-core": "file:../core",
|
"@google/gemini-cli-core": "file:../core",
|
||||||
@@ -18340,7 +18317,7 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode-ide-companion": {
|
"packages/vscode-ide-companion": {
|
||||||
"name": "gemini-cli-vscode-ide-companion",
|
"name": "gemini-cli-vscode-ide-companion",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"license": "LICENSE",
|
"license": "LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
|
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-a2a-server",
|
"name": "@google/gemini-cli-a2a-server",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"description": "Gemini CLI A2A Server",
|
"description": "Gemini CLI A2A Server",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"description": "Gemini CLI",
|
"description": "Gemini CLI",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
|
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "^0.12.0",
|
"@agentclientprotocol/sdk": "^0.12.0",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
type ExtensionSetting,
|
type ExtensionSetting,
|
||||||
} from '../../config/extensions/extensionSettings.js';
|
} from '../../config/extensions/extensionSettings.js';
|
||||||
import prompts from 'prompts';
|
import prompts from 'prompts';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mockExtensionManager,
|
mockExtensionManager,
|
||||||
@@ -79,11 +80,15 @@ vi.mock('../../config/settings.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('extensions configure command', () => {
|
describe('extensions configure command', () => {
|
||||||
|
let tempWorkspaceDir: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(debugLogger, 'log');
|
vi.spyOn(debugLogger, 'log');
|
||||||
vi.spyOn(debugLogger, 'error');
|
vi.spyOn(debugLogger, 'error');
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
tempWorkspaceDir = fs.mkdtempSync('gemini-cli-test-workspace');
|
||||||
|
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||||
// Default behaviors
|
// Default behaviors
|
||||||
mockLoadSettings.mockReturnValue({ merged: {} });
|
mockLoadSettings.mockReturnValue({ merged: {} });
|
||||||
mockGetExtensionAndManager.mockResolvedValue({
|
mockGetExtensionAndManager.mockResolvedValue({
|
||||||
@@ -141,6 +146,7 @@ describe('extensions configure command', () => {
|
|||||||
'TEST_VAR',
|
'TEST_VAR',
|
||||||
promptForSetting,
|
promptForSetting,
|
||||||
'user',
|
'user',
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,6 +192,7 @@ describe('extensions configure command', () => {
|
|||||||
'VAR_1',
|
'VAR_1',
|
||||||
promptForSetting,
|
promptForSetting,
|
||||||
'user',
|
'user',
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ async function configureSpecificSetting(
|
|||||||
settingKey,
|
settingKey,
|
||||||
promptForSetting,
|
promptForSetting,
|
||||||
scope,
|
scope,
|
||||||
|
process.cwd(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +219,7 @@ async function configureExtensionSettings(
|
|||||||
setting.envVar,
|
setting.envVar,
|
||||||
promptForSetting,
|
promptForSetting,
|
||||||
scope,
|
scope,
|
||||||
|
process.cwd(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ System using model: \${MODEL_NAME}
|
|||||||
'MY_VALUE',
|
'MY_VALUE',
|
||||||
mockRequestSetting,
|
mockRequestSetting,
|
||||||
ExtensionSettingScope.USER,
|
ExtensionSettingScope.USER,
|
||||||
|
process.cwd(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await extensionManager.restartExtension(extension);
|
await extensionManager.restartExtension(extension);
|
||||||
|
|||||||
@@ -398,6 +398,35 @@ describe('extensionSettings', () => {
|
|||||||
expect(actualContent).toBe('VAR1="a value with spaces"\n');
|
expect(actualContent).toBe('VAR1="a value with spaces"\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not set sensitive settings if the value is empty during initial setup', async () => {
|
||||||
|
const config: ExtensionConfig = {
|
||||||
|
name: 'test-ext',
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
name: 's1',
|
||||||
|
description: 'd1',
|
||||||
|
envVar: 'SENSITIVE_VAR',
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
mockRequestSetting.mockResolvedValue('');
|
||||||
|
|
||||||
|
await maybePromptForSettings(
|
||||||
|
config,
|
||||||
|
'12345',
|
||||||
|
mockRequestSetting,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userKeychain = new KeychainTokenStorage(
|
||||||
|
`Gemini CLI Extensions test-ext 12345`,
|
||||||
|
);
|
||||||
|
expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('should not attempt to clear secrets if keychain is unavailable', async () => {
|
it('should not attempt to clear secrets if keychain is unavailable', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockIsAvailable = vi.fn().mockResolvedValue(false);
|
const mockIsAvailable = vi.fn().mockResolvedValue(false);
|
||||||
@@ -738,5 +767,42 @@ describe('extensionSettings', () => {
|
|||||||
const lines = actualContent.split('\n').filter((line) => line.length > 0);
|
const lines = actualContent.split('\n').filter((line) => line.length > 0);
|
||||||
expect(lines).toHaveLength(3); // Should only have the three variables
|
expect(lines).toHaveLength(3); // Should only have the three variables
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should delete a sensitive setting if the new value is empty', async () => {
|
||||||
|
mockRequestSetting.mockResolvedValue('');
|
||||||
|
|
||||||
|
await updateSetting(
|
||||||
|
config,
|
||||||
|
'12345',
|
||||||
|
'VAR2',
|
||||||
|
mockRequestSetting,
|
||||||
|
ExtensionSettingScope.USER,
|
||||||
|
tempWorkspaceDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userKeychain = new KeychainTokenStorage(
|
||||||
|
`Gemini CLI Extensions test-ext 12345`,
|
||||||
|
);
|
||||||
|
expect(await userKeychain.getSecret('VAR2')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw if deleting a non-existent sensitive setting with empty value', async () => {
|
||||||
|
mockRequestSetting.mockResolvedValue('');
|
||||||
|
// Ensure it doesn't exist first
|
||||||
|
const userKeychain = new KeychainTokenStorage(
|
||||||
|
`Gemini CLI Extensions test-ext 12345`,
|
||||||
|
);
|
||||||
|
await userKeychain.deleteSecret('VAR2');
|
||||||
|
|
||||||
|
await updateSetting(
|
||||||
|
config,
|
||||||
|
'12345',
|
||||||
|
'VAR2',
|
||||||
|
mockRequestSetting,
|
||||||
|
ExtensionSettingScope.USER,
|
||||||
|
tempWorkspaceDir,
|
||||||
|
);
|
||||||
|
// Should complete without error
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export async function maybePromptForSettings(
|
|||||||
const nonSensitiveSettings: Record<string, string> = {};
|
const nonSensitiveSettings: Record<string, string> = {};
|
||||||
for (const setting of settings) {
|
for (const setting of settings) {
|
||||||
const value = allSettings[setting.envVar];
|
const value = allSettings[setting.envVar];
|
||||||
if (value === undefined) {
|
if (value === undefined || value === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (setting.sensitive) {
|
if (setting.sensitive) {
|
||||||
@@ -207,7 +207,7 @@ export async function updateSetting(
|
|||||||
settingKey: string,
|
settingKey: string,
|
||||||
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
||||||
scope: ExtensionSettingScope,
|
scope: ExtensionSettingScope,
|
||||||
workspaceDir?: string,
|
workspaceDir: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { name: extensionName, settings } = extensionConfig;
|
const { name: extensionName, settings } = extensionConfig;
|
||||||
if (!settings || settings.length === 0) {
|
if (!settings || settings.length === 0) {
|
||||||
@@ -230,7 +230,15 @@ export async function updateSetting(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (settingToUpdate.sensitive) {
|
if (settingToUpdate.sensitive) {
|
||||||
await keychain.setSecret(settingToUpdate.envVar, newValue);
|
if (newValue) {
|
||||||
|
await keychain.setSecret(settingToUpdate.envVar, newValue);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await keychain.deleteSecret(settingToUpdate.envVar);
|
||||||
|
} catch {
|
||||||
|
// Ignore if secret does not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1602,11 +1602,11 @@ const SETTINGS_SCHEMA = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Enable Hooks',
|
label: 'Enable Hooks',
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
requiresRestart: false,
|
requiresRestart: true,
|
||||||
default: true,
|
default: true,
|
||||||
description:
|
description:
|
||||||
'Canonical toggle for the hooks system. When disabled, no hooks will be executed.',
|
'Canonical toggle for the hooks system. When disabled, no hooks will be executed.',
|
||||||
showInDialog: false,
|
showInDialog: true,
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|||||||
@@ -1384,6 +1384,57 @@ describe('useTextBuffer', () => {
|
|||||||
expect(state.visualCursor).toEqual([0, 1]);
|
expect(state.visualCursor).toEqual([0, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('move: up/down should work on wrapped lines (regression test)', () => {
|
||||||
|
// Line that wraps into two visual lines
|
||||||
|
// Viewport width 10. "0123456789ABCDE" (15 chars)
|
||||||
|
// Visual Line 0: "0123456789"
|
||||||
|
// Visual Line 1: "ABCDE"
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useTextBuffer({
|
||||||
|
viewport: { width: 10, height: 5 },
|
||||||
|
isValidPath: () => false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setText('0123456789ABCDE');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cursor should be at the end: logical [0, 15], visual [1, 5]
|
||||||
|
expect(getBufferState(result).cursor).toEqual([0, 15]);
|
||||||
|
expect(getBufferState(result).visualCursor).toEqual([1, 5]);
|
||||||
|
|
||||||
|
// Press Up arrow - should move to first visual line
|
||||||
|
// This currently fails because handleInput returns false if cursorRow === 0
|
||||||
|
act(() => {
|
||||||
|
result.current.handleInput({
|
||||||
|
name: 'up',
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
ctrl: false,
|
||||||
|
cmd: false,
|
||||||
|
insertable: false,
|
||||||
|
sequence: '\x1b[A',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(getBufferState(result).visualCursor[0]).toBe(0);
|
||||||
|
|
||||||
|
// Press Down arrow - should move back to second visual line
|
||||||
|
// This would also fail if cursorRow is the last logical row
|
||||||
|
act(() => {
|
||||||
|
result.current.handleInput({
|
||||||
|
name: 'down',
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
ctrl: false,
|
||||||
|
cmd: false,
|
||||||
|
insertable: false,
|
||||||
|
sequence: '\x1b[B',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(getBufferState(result).visualCursor[0]).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('moveToVisualPosition: should correctly handle wide characters (Chinese)', () => {
|
it('moveToVisualPosition: should correctly handle wide characters (Chinese)', () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useTextBuffer({
|
useTextBuffer({
|
||||||
|
|||||||
@@ -2905,12 +2905,12 @@ export function useTextBuffer({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyMatchers[Command.MOVE_UP](key)) {
|
if (keyMatchers[Command.MOVE_UP](key)) {
|
||||||
if (cursorRow === 0) return false;
|
if (visualCursor[0] === 0) return false;
|
||||||
move('up');
|
move('up');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyMatchers[Command.MOVE_DOWN](key)) {
|
if (keyMatchers[Command.MOVE_DOWN](key)) {
|
||||||
if (cursorRow === lines.length - 1) return false;
|
if (visualCursor[0] === visualLines.length - 1) return false;
|
||||||
move('down');
|
move('down');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2974,6 +2974,8 @@ export function useTextBuffer({
|
|||||||
cursorCol,
|
cursorCol,
|
||||||
lines,
|
lines,
|
||||||
singleLine,
|
singleLine,
|
||||||
|
visualCursor,
|
||||||
|
visualLines,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -305,6 +305,9 @@ describe('clipboardUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null if tool is not yet detected', async () => {
|
it('should return null if tool is not yet detected', async () => {
|
||||||
|
// Unset session type to ensure no tool is detected automatically
|
||||||
|
delete process.env['XDG_SESSION_TYPE'];
|
||||||
|
|
||||||
// Don't prime the tool
|
// Don't prime the tool
|
||||||
const result = await clipboardUtils.saveClipboardImage(mockTargetDir);
|
const result = await clipboardUtils.saveClipboardImage(mockTargetDir);
|
||||||
expect(result).toBe(null);
|
expect(result).toBe(null);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-core",
|
"name": "@google/gemini-cli-core",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"description": "Gemini CLI Core",
|
"description": "Gemini CLI Core",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -312,6 +312,32 @@ describe('setupUser for new user', () => {
|
|||||||
userTierName: 'paid',
|
userTierName: 'paid',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw ineligible tier error when onboarding fails and ineligible tiers exist', async () => {
|
||||||
|
vi.stubEnv('GOOGLE_CLOUD_PROJECT', '');
|
||||||
|
mockLoad.mockResolvedValue({
|
||||||
|
allowedTiers: [mockPaidTier],
|
||||||
|
ineligibleTiers: [
|
||||||
|
{
|
||||||
|
reasonCode: 'UNSUPPORTED_LOCATION',
|
||||||
|
reasonMessage:
|
||||||
|
'Your current account is not eligible for Gemini Code Assist for individuals because it is not currently available in your location.',
|
||||||
|
tierId: 'free-tier',
|
||||||
|
tierName: 'Gemini Code Assist for individuals',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mockOnboardUser.mockResolvedValue({
|
||||||
|
done: true,
|
||||||
|
response: {
|
||||||
|
cloudaicompanionProject: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(setupUser({} as OAuth2Client)).rejects.toThrow(
|
||||||
|
'Your current account is not eligible for Gemini Code Assist for individuals because it is not currently available in your location.',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setupUser validation', () => {
|
describe('setupUser validation', () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ClientMetadata,
|
ClientMetadata,
|
||||||
GeminiUserTier,
|
GeminiUserTier,
|
||||||
|
IneligibleTier,
|
||||||
LoadCodeAssistResponse,
|
LoadCodeAssistResponse,
|
||||||
OnboardUserRequest,
|
OnboardUserRequest,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
@@ -35,6 +36,16 @@ export class ValidationCancelledError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class IneligibleTierError extends Error {
|
||||||
|
readonly ineligibleTiers: IneligibleTier[];
|
||||||
|
|
||||||
|
constructor(ineligibleTiers: IneligibleTier[]) {
|
||||||
|
const reasons = ineligibleTiers.map((t) => t.reasonMessage).join(', ');
|
||||||
|
super(reasons);
|
||||||
|
this.ineligibleTiers = ineligibleTiers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserData {
|
export interface UserData {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
userTier: UserTierId;
|
userTier: UserTierId;
|
||||||
@@ -127,13 +138,7 @@ export async function setupUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If user is not setup for standard tier, inform them about all other tiers they are ineligible for.
|
// If user is not setup for standard tier, inform them about all other tiers they are ineligible for.
|
||||||
if (loadRes.ineligibleTiers && loadRes.ineligibleTiers.length > 0) {
|
throwIneligibleOrProjectIdError(loadRes);
|
||||||
const reasons = loadRes.ineligibleTiers
|
|
||||||
.map((t) => t.reasonMessage)
|
|
||||||
.join(', ');
|
|
||||||
throw new Error(reasons);
|
|
||||||
}
|
|
||||||
throw new ProjectIdRequiredError();
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
projectId: loadRes.cloudaicompanionProject,
|
projectId: loadRes.cloudaicompanionProject,
|
||||||
@@ -180,7 +185,8 @@ export async function setupUser(
|
|||||||
userTierName: tier.name,
|
userTierName: tier.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new ProjectIdRequiredError();
|
|
||||||
|
throwIneligibleOrProjectIdError(loadRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -190,6 +196,13 @@ export async function setupUser(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function throwIneligibleOrProjectIdError(res: LoadCodeAssistResponse): never {
|
||||||
|
if (res.ineligibleTiers && res.ineligibleTiers.length > 0) {
|
||||||
|
throw new IneligibleTierError(res.ineligibleTiers);
|
||||||
|
}
|
||||||
|
throw new ProjectIdRequiredError();
|
||||||
|
}
|
||||||
|
|
||||||
function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier {
|
function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier {
|
||||||
for (const tier of res.allowedTiers || []) {
|
for (const tier of res.allowedTiers || []) {
|
||||||
if (tier.isDefault) {
|
if (tier.isDefault) {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { WriteFileTool } from '../tools/write-file.js';
|
|||||||
import { WebFetchTool } from '../tools/web-fetch.js';
|
import { WebFetchTool } from '../tools/web-fetch.js';
|
||||||
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
||||||
import { WebSearchTool } from '../tools/web-search.js';
|
import { WebSearchTool } from '../tools/web-search.js';
|
||||||
import { AskUserTool } from '../tools/ask-user.js';
|
|
||||||
import { GeminiClient } from '../core/client.js';
|
import { GeminiClient } from '../core/client.js';
|
||||||
import { BaseLlmClient } from '../core/baseLlmClient.js';
|
import { BaseLlmClient } from '../core/baseLlmClient.js';
|
||||||
import type { HookDefinition, HookEventName } from '../hooks/types.js';
|
import type { HookDefinition, HookEventName } from '../hooks/types.js';
|
||||||
@@ -1712,10 +1711,6 @@ export class Config {
|
|||||||
* @returns true if the path is allowed, false otherwise.
|
* @returns true if the path is allowed, false otherwise.
|
||||||
*/
|
*/
|
||||||
isPathAllowed(absolutePath: string): boolean {
|
isPathAllowed(absolutePath: string): boolean {
|
||||||
if (this.interactive && path.isAbsolute(absolutePath)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const realpath = (p: string) => {
|
const realpath = (p: string) => {
|
||||||
let resolved: string;
|
let resolved: string;
|
||||||
try {
|
try {
|
||||||
@@ -2073,7 +2068,6 @@ export class Config {
|
|||||||
registerCoreTool(ShellTool, this);
|
registerCoreTool(ShellTool, this);
|
||||||
registerCoreTool(MemoryTool);
|
registerCoreTool(MemoryTool);
|
||||||
registerCoreTool(WebSearchTool, this);
|
registerCoreTool(WebSearchTool, this);
|
||||||
registerCoreTool(AskUserTool);
|
|
||||||
if (this.getUseWriteTodos()) {
|
if (this.getUseWriteTodos()) {
|
||||||
registerCoreTool(WriteTodosTool);
|
registerCoreTool(WriteTodosTool);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,8 +185,6 @@ The following read-only tools are available in Plan Mode:
|
|||||||
|
|
||||||
### Phase 1: Requirements Understanding
|
### Phase 1: Requirements Understanding
|
||||||
- Analyze the user's request to identify core requirements and constraints
|
- Analyze the user's request to identify core requirements and constraints
|
||||||
- If critical information is missing or ambiguous, ask clarifying questions using the \`ask_user\` tool
|
|
||||||
- When using \`ask_user\`, prefer providing multiple-choice options for the user to select from when possible
|
|
||||||
- Do NOT explore the project or create a plan yet
|
- Do NOT explore the project or create a plan yet
|
||||||
|
|
||||||
### Phase 2: Project Exploration
|
### Phase 2: Project Exploration
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ import type {
|
|||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
import { toParts } from '../code_assist/converter.js';
|
import { toParts } from '../code_assist/converter.js';
|
||||||
import { createUserContent, FinishReason } from '@google/genai';
|
import { createUserContent, FinishReason } from '@google/genai';
|
||||||
import { retryWithBackoff, isRetryableError } from '../utils/retry.js';
|
import {
|
||||||
|
retryWithBackoff,
|
||||||
|
isRetryableError,
|
||||||
|
DEFAULT_MAX_ATTEMPTS,
|
||||||
|
} from '../utils/retry.js';
|
||||||
import type { ValidationRequiredError } from '../utils/googleQuotaErrors.js';
|
import type { ValidationRequiredError } from '../utils/googleQuotaErrors.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import {
|
import {
|
||||||
@@ -606,7 +610,7 @@ export class GeminiChat {
|
|||||||
onRetry: (attempt, error, delayMs) => {
|
onRetry: (attempt, error, delayMs) => {
|
||||||
coreEvents.emitRetryAttempt({
|
coreEvents.emitRetryAttempt({
|
||||||
attempt,
|
attempt,
|
||||||
maxAttempts: availabilityMaxAttempts ?? 10,
|
maxAttempts: availabilityMaxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
||||||
delayMs,
|
delayMs,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
model: lastModelToUse,
|
model: lastModelToUse,
|
||||||
|
|||||||
@@ -274,11 +274,10 @@ describe('Core System Prompt (prompts.ts)', () => {
|
|||||||
|
|
||||||
it('should only list available tools in PLAN mode', () => {
|
it('should only list available tools in PLAN mode', () => {
|
||||||
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.PLAN);
|
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.PLAN);
|
||||||
// Only enable a subset of tools, including ask_user
|
// Only enable a subset of tools
|
||||||
vi.mocked(mockConfig.getToolRegistry().getAllToolNames).mockReturnValue([
|
vi.mocked(mockConfig.getToolRegistry().getAllToolNames).mockReturnValue([
|
||||||
'glob',
|
'glob',
|
||||||
'read_file',
|
'read_file',
|
||||||
'ask_user',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const prompt = getCoreSystemPrompt(mockConfig);
|
const prompt = getCoreSystemPrompt(mockConfig);
|
||||||
@@ -286,7 +285,6 @@ describe('Core System Prompt (prompts.ts)', () => {
|
|||||||
// Should include enabled tools
|
// Should include enabled tools
|
||||||
expect(prompt).toContain('`glob`');
|
expect(prompt).toContain('`glob`');
|
||||||
expect(prompt).toContain('`read_file`');
|
expect(prompt).toContain('`read_file`');
|
||||||
expect(prompt).toContain('`ask_user`');
|
|
||||||
|
|
||||||
// Should NOT include disabled tools
|
// Should NOT include disabled tools
|
||||||
expect(prompt).not.toContain('`google_web_search`');
|
expect(prompt).not.toContain('`google_web_search`');
|
||||||
|
|||||||
@@ -64,12 +64,6 @@ decision = "allow"
|
|||||||
priority = 50
|
priority = 50
|
||||||
modes = ["plan"]
|
modes = ["plan"]
|
||||||
|
|
||||||
[[rule]]
|
|
||||||
toolName = "ask_user"
|
|
||||||
decision = "allow"
|
|
||||||
priority = 50
|
|
||||||
modes = ["plan"]
|
|
||||||
|
|
||||||
# Allow write_file for .md files in plans directory
|
# Allow write_file for .md files in plans directory
|
||||||
[[rule]]
|
[[rule]]
|
||||||
toolName = "write_file"
|
toolName = "write_file"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ACTIVATE_SKILL_TOOL_NAME,
|
ACTIVATE_SKILL_TOOL_NAME,
|
||||||
ASK_USER_TOOL_NAME,
|
|
||||||
EDIT_TOOL_NAME,
|
EDIT_TOOL_NAME,
|
||||||
GLOB_TOOL_NAME,
|
GLOB_TOOL_NAME,
|
||||||
GREP_TOOL_NAME,
|
GREP_TOOL_NAME,
|
||||||
@@ -313,8 +312,6 @@ ${options.planModeToolsList}
|
|||||||
|
|
||||||
### Phase 1: Requirements Understanding
|
### Phase 1: Requirements Understanding
|
||||||
- Analyze the user's request to identify core requirements and constraints
|
- Analyze the user's request to identify core requirements and constraints
|
||||||
- If critical information is missing or ambiguous, ask clarifying questions using the \`${ASK_USER_TOOL_NAME}\` tool
|
|
||||||
- When using \`${ASK_USER_TOOL_NAME}\`, prefer providing multiple-choice options for the user to select from when possible
|
|
||||||
- Do NOT explore the project or create a plan yet
|
- Do NOT explore the project or create a plan yet
|
||||||
|
|
||||||
### Phase 2: Project Exploration
|
### Phase 2: Project Exploration
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_FLASH_MODEL,
|
model: DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Control)',
|
source: 'NumericalClassifier (Control)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 40 / Threshold: 50'),
|
reasoning: expect.stringContaining('Score: 40 / Threshold: 50'),
|
||||||
},
|
},
|
||||||
@@ -148,7 +148,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_MODEL,
|
model: DEFAULT_GEMINI_MODEL,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Control)',
|
source: 'NumericalClassifier (Control)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 60 / Threshold: 50'),
|
reasoning: expect.stringContaining('Score: 60 / Threshold: 50'),
|
||||||
},
|
},
|
||||||
@@ -174,7 +174,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_FLASH_MODEL, // Routed to Flash because 60 < 80
|
model: DEFAULT_GEMINI_FLASH_MODEL, // Routed to Flash because 60 < 80
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Strict)',
|
source: 'NumericalClassifier (Strict)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 60 / Threshold: 80'),
|
reasoning: expect.stringContaining('Score: 60 / Threshold: 80'),
|
||||||
},
|
},
|
||||||
@@ -200,7 +200,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_MODEL,
|
model: DEFAULT_GEMINI_MODEL,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Strict)',
|
source: 'NumericalClassifier (Strict)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 90 / Threshold: 80'),
|
reasoning: expect.stringContaining('Score: 90 / Threshold: 80'),
|
||||||
},
|
},
|
||||||
@@ -228,7 +228,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_FLASH_MODEL, // Score 60 < Threshold 70
|
model: DEFAULT_GEMINI_FLASH_MODEL, // Score 60 < Threshold 70
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Remote)',
|
source: 'NumericalClassifier (Remote)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 60 / Threshold: 70'),
|
reasoning: expect.stringContaining('Score: 60 / Threshold: 70'),
|
||||||
},
|
},
|
||||||
@@ -254,7 +254,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_FLASH_MODEL, // Score 40 < Threshold 45.5
|
model: DEFAULT_GEMINI_FLASH_MODEL, // Score 40 < Threshold 45.5
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Remote)',
|
source: 'NumericalClassifier (Remote)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 40 / Threshold: 45.5'),
|
reasoning: expect.stringContaining('Score: 40 / Threshold: 45.5'),
|
||||||
},
|
},
|
||||||
@@ -280,7 +280,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_MODEL, // Score 35 >= Threshold 30
|
model: DEFAULT_GEMINI_MODEL, // Score 35 >= Threshold 30
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Remote)',
|
source: 'NumericalClassifier (Remote)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 35 / Threshold: 30'),
|
reasoning: expect.stringContaining('Score: 35 / Threshold: 30'),
|
||||||
},
|
},
|
||||||
@@ -308,7 +308,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_FLASH_MODEL, // Score 40 < Default A/B Threshold 50
|
model: DEFAULT_GEMINI_FLASH_MODEL, // Score 40 < Default A/B Threshold 50
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Control)',
|
source: 'NumericalClassifier (Control)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 40 / Threshold: 50'),
|
reasoning: expect.stringContaining('Score: 40 / Threshold: 50'),
|
||||||
},
|
},
|
||||||
@@ -335,7 +335,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_FLASH_MODEL,
|
model: DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Control)',
|
source: 'NumericalClassifier (Control)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 40 / Threshold: 50'),
|
reasoning: expect.stringContaining('Score: 40 / Threshold: 50'),
|
||||||
},
|
},
|
||||||
@@ -362,7 +362,7 @@ describe('NumericalClassifierStrategy', () => {
|
|||||||
expect(decision).toEqual({
|
expect(decision).toEqual({
|
||||||
model: DEFAULT_GEMINI_MODEL,
|
model: DEFAULT_GEMINI_MODEL,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'Classifier (Control)',
|
source: 'NumericalClassifier (Control)',
|
||||||
latencyMs: expect.any(Number),
|
latencyMs: expect.any(Number),
|
||||||
reasoning: expect.stringContaining('Score: 60 / Threshold: 50'),
|
reasoning: expect.stringContaining('Score: 60 / Threshold: 50'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
|
|||||||
return {
|
return {
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: `Classifier (${groupLabel})`,
|
source: `NumericalClassifier (${groupLabel})`,
|
||||||
latencyMs,
|
latencyMs,
|
||||||
reasoning: `[Score: ${score} / Threshold: ${threshold}] ${routerResponse.complexity_reasoning}`,
|
reasoning: `[Score: ${score} / Threshold: ${threshold}] ${routerResponse.complexity_reasoning}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -930,6 +930,38 @@ describe('ClearcutLogger', () => {
|
|||||||
'Something went wrong',
|
'Something went wrong',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('logs a successful routing event with numerical routing fields', () => {
|
||||||
|
const { logger } = setup();
|
||||||
|
const event = new ModelRoutingEvent(
|
||||||
|
'gemini-pro',
|
||||||
|
'NumericalClassifier (Strict)',
|
||||||
|
123,
|
||||||
|
'[Score: 90 / Threshold: 80] reasoning',
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
'80',
|
||||||
|
);
|
||||||
|
|
||||||
|
logger?.logModelRoutingEvent(event);
|
||||||
|
|
||||||
|
const events = getEvents(logger!);
|
||||||
|
expect(events.length).toBe(1);
|
||||||
|
expect(events[0]).toHaveEventName(EventNames.MODEL_ROUTING);
|
||||||
|
expect(events[0]).toHaveMetadataValue([
|
||||||
|
EventMetadataKey.GEMINI_CLI_ROUTING_REASONING,
|
||||||
|
'[Score: 90 / Threshold: 80] reasoning',
|
||||||
|
]);
|
||||||
|
expect(events[0]).toHaveMetadataValue([
|
||||||
|
EventMetadataKey.GEMINI_CLI_ROUTING_NUMERICAL_ENABLED,
|
||||||
|
'true',
|
||||||
|
]);
|
||||||
|
expect(events[0]).toHaveMetadataValue([
|
||||||
|
EventMetadataKey.GEMINI_CLI_ROUTING_CLASSIFIER_THRESHOLD,
|
||||||
|
'80',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('logAgentStartEvent', () => {
|
describe('logAgentStartEvent', () => {
|
||||||
|
|||||||
@@ -1234,6 +1234,28 @@ export class ClearcutLogger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.reasoning && this.config?.getTelemetryLogPromptsEnabled()) {
|
||||||
|
data.push({
|
||||||
|
gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_REASONING,
|
||||||
|
value: event.reasoning,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.enable_numerical_routing !== undefined) {
|
||||||
|
data.push({
|
||||||
|
gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_NUMERICAL_ENABLED,
|
||||||
|
value: event.enable_numerical_routing.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.classifier_threshold) {
|
||||||
|
data.push({
|
||||||
|
gemini_cli_key:
|
||||||
|
EventMetadataKey.GEMINI_CLI_ROUTING_CLASSIFIER_THRESHOLD,
|
||||||
|
value: event.classifier_threshold,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.enqueueLogEvent(this.createLogEvent(EventNames.MODEL_ROUTING, data));
|
this.enqueueLogEvent(this.createLogEvent(EventNames.MODEL_ROUTING, data));
|
||||||
this.flushIfNeeded();
|
this.flushIfNeeded();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// Defines valid event metadata keys for Clearcut logging.
|
// Defines valid event metadata keys for Clearcut logging.
|
||||||
export enum EventMetadataKey {
|
export enum EventMetadataKey {
|
||||||
// Deleted enums: 24
|
// Deleted enums: 24
|
||||||
// Next ID: 144
|
// Next ID: 148
|
||||||
|
|
||||||
GEMINI_CLI_KEY_UNKNOWN = 0,
|
GEMINI_CLI_KEY_UNKNOWN = 0,
|
||||||
|
|
||||||
@@ -542,4 +542,17 @@ export enum EventMetadataKey {
|
|||||||
|
|
||||||
// Logs the duration spent in an approval mode in milliseconds.
|
// Logs the duration spent in an approval mode in milliseconds.
|
||||||
GEMINI_CLI_APPROVAL_MODE_DURATION_MS = 143,
|
GEMINI_CLI_APPROVAL_MODE_DURATION_MS = 143,
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Model Routing Event Keys (Cont.)
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
// Logs the reasoning for the routing decision.
|
||||||
|
GEMINI_CLI_ROUTING_REASONING = 145,
|
||||||
|
|
||||||
|
// Logs whether numerical routing was enabled.
|
||||||
|
GEMINI_CLI_ROUTING_NUMERICAL_ENABLED = 146,
|
||||||
|
|
||||||
|
// Logs the classifier threshold used.
|
||||||
|
GEMINI_CLI_ROUTING_CLASSIFIER_THRESHOLD = 147,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1734,6 +1734,37 @@ describe('loggers', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should log the event with numerical routing fields', () => {
|
||||||
|
const event = new ModelRoutingEvent(
|
||||||
|
'gemini-pro',
|
||||||
|
'NumericalClassifier (Strict)',
|
||||||
|
150,
|
||||||
|
'[Score: 90 / Threshold: 80] reasoning',
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
'80',
|
||||||
|
);
|
||||||
|
|
||||||
|
logModelRouting(mockConfig, event);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
ClearcutLogger.prototype.logModelRoutingEvent,
|
||||||
|
).toHaveBeenCalledWith(event);
|
||||||
|
|
||||||
|
expect(mockLogger.emit).toHaveBeenCalledWith({
|
||||||
|
body: 'Model routing decision. Model: gemini-pro, Source: NumericalClassifier (Strict)',
|
||||||
|
attributes: {
|
||||||
|
'session.id': 'test-session-id',
|
||||||
|
'user.email': 'test-user@example.com',
|
||||||
|
'installation.id': 'test-installation-id',
|
||||||
|
...event,
|
||||||
|
'event.name': EVENT_MODEL_ROUTING,
|
||||||
|
interactive: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should only log to Clearcut if OTEL SDK is not initialized', () => {
|
it('should only log to Clearcut if OTEL SDK is not initialized', () => {
|
||||||
vi.spyOn(sdk, 'isTelemetrySdkInitialized').mockReturnValue(false);
|
vi.spyOn(sdk, 'isTelemetrySdkInitialized').mockReturnValue(false);
|
||||||
vi.spyOn(sdk, 'bufferTelemetryEvent').mockImplementation(() => {});
|
vi.spyOn(sdk, 'bufferTelemetryEvent').mockImplementation(() => {});
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ describe('Telemetry Metrics', () => {
|
|||||||
initializeMetricsModule(mockConfig);
|
initializeMetricsModule(mockConfig);
|
||||||
const event = new ModelRoutingEvent(
|
const event = new ModelRoutingEvent(
|
||||||
'gemini-pro',
|
'gemini-pro',
|
||||||
'classifier',
|
'Classifier',
|
||||||
200,
|
200,
|
||||||
'test-reason',
|
'test-reason',
|
||||||
true,
|
true,
|
||||||
@@ -502,7 +502,7 @@ describe('Telemetry Metrics', () => {
|
|||||||
'installation.id': 'test-installation-id',
|
'installation.id': 'test-installation-id',
|
||||||
'user.email': 'test@example.com',
|
'user.email': 'test@example.com',
|
||||||
'routing.decision_model': 'gemini-pro',
|
'routing.decision_model': 'gemini-pro',
|
||||||
'routing.decision_source': 'classifier',
|
'routing.decision_source': 'Classifier',
|
||||||
'routing.failed': true,
|
'routing.failed': true,
|
||||||
'routing.reasoning': 'test-reason',
|
'routing.reasoning': 'test-reason',
|
||||||
});
|
});
|
||||||
@@ -513,7 +513,7 @@ describe('Telemetry Metrics', () => {
|
|||||||
'installation.id': 'test-installation-id',
|
'installation.id': 'test-installation-id',
|
||||||
'user.email': 'test@example.com',
|
'user.email': 'test@example.com',
|
||||||
'routing.decision_model': 'gemini-pro',
|
'routing.decision_model': 'gemini-pro',
|
||||||
'routing.decision_source': 'classifier',
|
'routing.decision_source': 'Classifier',
|
||||||
'routing.failed': true,
|
'routing.failed': true,
|
||||||
'routing.reasoning': 'test-reason',
|
'routing.reasoning': 'test-reason',
|
||||||
'routing.error_message': 'test-error',
|
'routing.error_message': 'test-error',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { AuthProviderType } from '../config/config.js';
|
|||||||
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
|
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
|
||||||
import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';
|
import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';
|
||||||
import { DiscoveredMCPTool } from './mcp-tool.js';
|
import { DiscoveredMCPTool } from './mcp-tool.js';
|
||||||
|
import { XcodeMcpBridgeFixTransport } from './xcode-mcp-fix-transport.js';
|
||||||
|
|
||||||
import type { CallableTool, FunctionCall, Part, Tool } from '@google/genai';
|
import type { CallableTool, FunctionCall, Part, Tool } from '@google/genai';
|
||||||
import { basename } from 'node:path';
|
import { basename } from 'node:path';
|
||||||
@@ -1867,7 +1868,7 @@ export async function createTransport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mcpServerConfig.command) {
|
if (mcpServerConfig.command) {
|
||||||
const transport = new StdioClientTransport({
|
let transport: Transport = new StdioClientTransport({
|
||||||
command: mcpServerConfig.command,
|
command: mcpServerConfig.command,
|
||||||
args: mcpServerConfig.args || [],
|
args: mcpServerConfig.args || [],
|
||||||
env: {
|
env: {
|
||||||
@@ -1877,14 +1878,38 @@ export async function createTransport(
|
|||||||
cwd: mcpServerConfig.cwd,
|
cwd: mcpServerConfig.cwd,
|
||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fix for Xcode 26.3 mcpbridge non-compliant responses
|
||||||
|
// It returns JSON in `content` instead of `structuredContent`
|
||||||
|
if (
|
||||||
|
mcpServerConfig.command === 'xcrun' &&
|
||||||
|
mcpServerConfig.args?.includes('mcpbridge')
|
||||||
|
) {
|
||||||
|
transport = new XcodeMcpBridgeFixTransport(transport);
|
||||||
|
}
|
||||||
|
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
transport.stderr!.on('data', (data) => {
|
// The `XcodeMcpBridgeFixTransport` wrapper hides the underlying `StdioClientTransport`,
|
||||||
const stderrStr = data.toString().trim();
|
// which exposes `stderr` for debug logging. We need to unwrap it to attach the listener.
|
||||||
debugLogger.debug(
|
|
||||||
`[DEBUG] [MCP STDERR (${mcpServerName})]: `,
|
const underlyingTransport =
|
||||||
stderrStr,
|
transport instanceof XcodeMcpBridgeFixTransport
|
||||||
);
|
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
});
|
(transport as any).transport
|
||||||
|
: transport;
|
||||||
|
|
||||||
|
if (
|
||||||
|
underlyingTransport instanceof StdioClientTransport &&
|
||||||
|
underlyingTransport.stderr
|
||||||
|
) {
|
||||||
|
underlyingTransport.stderr.on('data', (data) => {
|
||||||
|
const stderrStr = data.toString().trim();
|
||||||
|
debugLogger.debug(
|
||||||
|
`[DEBUG] [MCP STDERR (${mcpServerName})]: `,
|
||||||
|
stderrStr,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return transport;
|
return transport;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export const ALL_BUILTIN_TOOL_NAMES = [
|
|||||||
LS_TOOL_NAME,
|
LS_TOOL_NAME,
|
||||||
MEMORY_TOOL_NAME,
|
MEMORY_TOOL_NAME,
|
||||||
ACTIVATE_SKILL_TOOL_NAME,
|
ACTIVATE_SKILL_TOOL_NAME,
|
||||||
ASK_USER_TOOL_NAME,
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +59,6 @@ export const PLAN_MODE_TOOLS = [
|
|||||||
READ_FILE_TOOL_NAME,
|
READ_FILE_TOOL_NAME,
|
||||||
LS_TOOL_NAME,
|
LS_TOOL_NAME,
|
||||||
WEB_SEARCH_TOOL_NAME,
|
WEB_SEARCH_TOOL_NAME,
|
||||||
ASK_USER_TOOL_NAME,
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import { XcodeMcpBridgeFixTransport } from './xcode-mcp-fix-transport.js';
|
||||||
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|
||||||
|
// Mock Transport that simulates the mcpbridge behavior
|
||||||
|
class MockBadMcpBridgeTransport extends EventEmitter implements Transport {
|
||||||
|
onclose?: () => void;
|
||||||
|
onerror?: (error: Error) => void;
|
||||||
|
onmessage?: (message: JSONRPCMessage) => void;
|
||||||
|
|
||||||
|
async start() {}
|
||||||
|
async close() {}
|
||||||
|
async send(_message: JSONRPCMessage) {}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
emitMessage(msg: any) {
|
||||||
|
this.onmessage?.(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Xcode MCP Bridge Fix', () => {
|
||||||
|
it('intercepts and fixes the non-compliant mcpbridge response', async () => {
|
||||||
|
const mockTransport = new MockBadMcpBridgeTransport();
|
||||||
|
const fixTransport = new XcodeMcpBridgeFixTransport(mockTransport);
|
||||||
|
|
||||||
|
// We need to capture what the fixTransport emits to its listeners
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const messages: any[] = [];
|
||||||
|
fixTransport.onmessage = (msg) => {
|
||||||
|
messages.push(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
await fixTransport.start();
|
||||||
|
|
||||||
|
// SCENARIO 1: Bad response from Xcode
|
||||||
|
// It has `content` stringified JSON, but misses `structuredContent`
|
||||||
|
const badPayload = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
result: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
windows: [{ title: 'HelloWorld', path: '/path/to/project' }],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Missing: structuredContent
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTransport.emitMessage(badPayload);
|
||||||
|
|
||||||
|
// Verify the message received by the client (listener of fixTransport)
|
||||||
|
const fixedMsg = messages.find((m) => m.id === 1);
|
||||||
|
expect(fixedMsg).toBeDefined();
|
||||||
|
expect(fixedMsg.result.structuredContent).toBeDefined();
|
||||||
|
expect(fixedMsg.result.structuredContent.windows[0].title).toBe(
|
||||||
|
'HelloWorld',
|
||||||
|
);
|
||||||
|
|
||||||
|
// SCENARIO 2: Good response (should be untouched)
|
||||||
|
const goodPayload = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 2,
|
||||||
|
result: {
|
||||||
|
content: [{ type: 'text', text: 'normal text' }],
|
||||||
|
structuredContent: { some: 'data' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockTransport.emitMessage(goodPayload);
|
||||||
|
|
||||||
|
const goodMsg = messages.find((m) => m.id === 2);
|
||||||
|
expect(goodMsg).toBeDefined();
|
||||||
|
expect(goodMsg.result.structuredContent).toEqual({ some: 'data' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores responses that cannot be parsed as JSON', async () => {
|
||||||
|
const mockTransport = new MockBadMcpBridgeTransport();
|
||||||
|
const fixTransport = new XcodeMcpBridgeFixTransport(mockTransport);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const messages: any[] = [];
|
||||||
|
fixTransport.onmessage = (msg) => {
|
||||||
|
messages.push(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
await fixTransport.start();
|
||||||
|
|
||||||
|
const nonJsonPayload = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 3,
|
||||||
|
result: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: "Just some plain text that isn't JSON",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockTransport.emitMessage(nonJsonPayload);
|
||||||
|
|
||||||
|
const msg = messages.find((m) => m.id === 3);
|
||||||
|
expect(msg).toBeDefined();
|
||||||
|
expect(msg.result.structuredContent).toBeUndefined();
|
||||||
|
expect(msg.result.content[0].text).toBe(
|
||||||
|
"Just some plain text that isn't JSON",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
|
import type {
|
||||||
|
JSONRPCMessage,
|
||||||
|
JSONRPCResponse,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper transport that intercepts messages from Xcode's mcpbridge and fixes
|
||||||
|
* non-compliant responses.
|
||||||
|
*
|
||||||
|
* Issue: Xcode 26.3's mcpbridge returns tool results in `content` but misses
|
||||||
|
* `structuredContent` when the tool has an output schema.
|
||||||
|
*
|
||||||
|
* Fix: Parse the text content as JSON and populate `structuredContent`.
|
||||||
|
*/
|
||||||
|
export class XcodeMcpBridgeFixTransport
|
||||||
|
extends EventEmitter
|
||||||
|
implements Transport
|
||||||
|
{
|
||||||
|
constructor(private readonly transport: Transport) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Forward messages from the underlying transport
|
||||||
|
this.transport.onmessage = (message) => {
|
||||||
|
this.handleMessage(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.transport.onclose = () => {
|
||||||
|
this.onclose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.transport.onerror = (error) => {
|
||||||
|
this.onerror?.(error);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transport interface implementation
|
||||||
|
onclose?: () => void;
|
||||||
|
onerror?: (error: Error) => void;
|
||||||
|
onmessage?: (message: JSONRPCMessage) => void;
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
await this.transport.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
await this.transport.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(message: JSONRPCMessage): Promise<void> {
|
||||||
|
await this.transport.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessage(message: JSONRPCMessage) {
|
||||||
|
if (this.isJsonResponse(message)) {
|
||||||
|
this.fixStructuredContent(message);
|
||||||
|
}
|
||||||
|
this.onmessage?.(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isJsonResponse(message: JSONRPCMessage): message is JSONRPCResponse {
|
||||||
|
return 'result' in message || 'error' in message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fixStructuredContent(response: JSONRPCResponse) {
|
||||||
|
if (!('result' in response)) return;
|
||||||
|
|
||||||
|
// We can cast because we verified 'result' is in response,
|
||||||
|
// but TS might still be picky if the type is a strict union.
|
||||||
|
// Let's treat it safely.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const result = response.result as any;
|
||||||
|
|
||||||
|
// Check if we have content but missing structuredContent
|
||||||
|
if (
|
||||||
|
result.content &&
|
||||||
|
Array.isArray(result.content) &&
|
||||||
|
result.content.length > 0 &&
|
||||||
|
!result.structuredContent
|
||||||
|
) {
|
||||||
|
const firstItem = result.content[0];
|
||||||
|
if (firstItem.type === 'text' && typeof firstItem.text === 'string') {
|
||||||
|
try {
|
||||||
|
// Attempt to parse the text as JSON
|
||||||
|
const parsed = JSON.parse(firstItem.text);
|
||||||
|
// If successful, populate structuredContent
|
||||||
|
result.structuredContent = parsed;
|
||||||
|
} catch (_) {
|
||||||
|
// Ignored: Content is likely plain text, not JSON.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,33 +101,33 @@ describe('retryWithBackoff', () => {
|
|||||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to 10 maxAttempts if no options are provided', async () => {
|
it('should default to 3 maxAttempts if no options are provided', async () => {
|
||||||
// This function will fail more than 10 times to ensure all retries are used.
|
// This function will fail more than 3 times to ensure all retries are used.
|
||||||
const mockFn = createFailingFunction(15);
|
const mockFn = createFailingFunction(5);
|
||||||
|
|
||||||
const promise = retryWithBackoff(mockFn);
|
const promise = retryWithBackoff(mockFn);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
expect(promise).rejects.toThrow('Simulated error attempt 10'),
|
expect(promise).rejects.toThrow('Simulated error attempt 3'),
|
||||||
vi.runAllTimersAsync(),
|
vi.runAllTimersAsync(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mockFn).toHaveBeenCalledTimes(10);
|
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to 10 maxAttempts if options.maxAttempts is undefined', async () => {
|
it('should default to 3 maxAttempts if options.maxAttempts is undefined', async () => {
|
||||||
// This function will fail more than 10 times to ensure all retries are used.
|
// This function will fail more than 3 times to ensure all retries are used.
|
||||||
const mockFn = createFailingFunction(15);
|
const mockFn = createFailingFunction(5);
|
||||||
|
|
||||||
const promise = retryWithBackoff(mockFn, { maxAttempts: undefined });
|
const promise = retryWithBackoff(mockFn, { maxAttempts: undefined });
|
||||||
|
|
||||||
// Expect it to fail with the error from the 10th attempt.
|
// Expect it to fail with the error from the 3rd attempt.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
expect(promise).rejects.toThrow('Simulated error attempt 10'),
|
expect(promise).rejects.toThrow('Simulated error attempt 3'),
|
||||||
vi.runAllTimersAsync(),
|
vi.runAllTimersAsync(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mockFn).toHaveBeenCalledTimes(10);
|
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not retry if shouldRetry returns false', async () => {
|
it('should not retry if shouldRetry returns false', async () => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { getErrorStatus, ModelNotFoundError } from './httpErrors.js';
|
|||||||
import type { RetryAvailabilityContext } from '../availability/modelPolicy.js';
|
import type { RetryAvailabilityContext } from '../availability/modelPolicy.js';
|
||||||
|
|
||||||
export type { RetryAvailabilityContext };
|
export type { RetryAvailabilityContext };
|
||||||
|
export const DEFAULT_MAX_ATTEMPTS = 3;
|
||||||
|
|
||||||
export interface RetryOptions {
|
export interface RetryOptions {
|
||||||
maxAttempts: number;
|
maxAttempts: number;
|
||||||
@@ -40,7 +41,7 @@ export interface RetryOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
|
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
|
||||||
maxAttempts: 10,
|
maxAttempts: DEFAULT_MAX_ATTEMPTS,
|
||||||
initialDelayMs: 5000,
|
initialDelayMs: 5000,
|
||||||
maxDelayMs: 30000, // 30 seconds
|
maxDelayMs: 30000, // 30 seconds
|
||||||
shouldRetryOnError: isRetryableError,
|
shouldRetryOnError: isRetryableError,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-test-utils",
|
"name": "@google/gemini-cli-test-utils",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "gemini-cli-vscode-ide-companion",
|
"name": "gemini-cli-vscode-ide-companion",
|
||||||
"displayName": "Gemini CLI Companion",
|
"displayName": "Gemini CLI Companion",
|
||||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
"version": "0.27.3",
|
||||||
"publisher": "google",
|
"publisher": "google",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1537,7 +1537,7 @@
|
|||||||
"enabled": {
|
"enabled": {
|
||||||
"title": "Enable Hooks",
|
"title": "Enable Hooks",
|
||||||
"description": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.",
|
"description": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.",
|
||||||
"markdownDescription": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `true`",
|
"markdownDescription": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `true`",
|
||||||
"default": true,
|
"default": true,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user