mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 00:01:24 -07:00
Compare commits
566 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad813890db | |||
| ddcbd0c2bd | |||
| ae51bbdae8 | |||
| ae387b61a9 | |||
| 6f6e004f82 | |||
| d37fff7fd6 | |||
| cea1a867b6 | |||
| d6933c77ba | |||
| 0c3fcb7030 | |||
| 94f43c79d0 | |||
| 5478b58166 | |||
| a49a09f13c | |||
| ac4a79223a | |||
| 042288e72c | |||
| 8a2c2dc73f | |||
| bf32492da9 | |||
| e8a0249e6b | |||
| ea061f52b0 | |||
| d1485d4672 | |||
| 62ba330612 | |||
| 1bd75f060d | |||
| 331e2ce45d | |||
| 0b2d79a2ea | |||
| ffcd996366 | |||
| 93694c6a65 | |||
| db51e3f4cd | |||
| 19400ba8c7 | |||
| eb1a6a6091 | |||
| 80a414be97 | |||
| 8d17d09488 | |||
| 38dccf32c1 | |||
| 24c15b9d43 | |||
| 2aa2ab878b | |||
| 969833e6e3 | |||
| 94b377049b | |||
| 0d22b22c82 | |||
| e909993dd1 | |||
| 3d7cb3fb8a | |||
| e8a065cb9f | |||
| 8a16165a9b | |||
| a4516665d5 | |||
| 11c995e9fa | |||
| 53434d860a | |||
| 2e4e53c3ee | |||
| 7e2ffd7a80 | |||
| 463e5d5b7e | |||
| 809b933d81 | |||
| 8bf8707668 | |||
| 18e5113754 | |||
| 70bc2933cc | |||
| 6535b71c35 | |||
| ed309096b1 | |||
| f885375c72 | |||
| d2d9ae3f91 | |||
| 5f080aa52a | |||
| c334f02d51 | |||
| 2d76cdf2c6 | |||
| defda3a97d | |||
| a0c8e3bf2b | |||
| e209724789 | |||
| 4caaa2a8e8 | |||
| c463d47fa8 | |||
| f80eb71068 | |||
| 135d3401cd | |||
| fab279f0fd | |||
| b1da8c210e | |||
| 9ba1640c07 | |||
| 422eb78b41 | |||
| 275a12fd45 | |||
| 8abe7e151c | |||
| 05c962af1f | |||
| 74447fcff2 | |||
| ee36307495 | |||
| 4c6da1eaf9 | |||
| 86e45c9aa5 | |||
| e0ba7e4ffb | |||
| 3660d4ecc0 | |||
| 22740ddceb | |||
| ad59be0c81 | |||
| 66c2184fe5 | |||
| e0ef5beae4 | |||
| 9d70649b75 | |||
| 4f49341ce9 | |||
| d8b895a2f1 | |||
| 1753c71bfa | |||
| b540137820 | |||
| 5cadd37eb2 | |||
| cc47e475af | |||
| 3667ecf107 | |||
| ce92ed3f68 | |||
| c6f8ecc227 | |||
| 38e053b7bb | |||
| 03bd6876d2 | |||
| 4bd4cd697b | |||
| 28c3901513 | |||
| c12b87ed03 | |||
| f795255e58 | |||
| 39b0948417 | |||
| 16278fd849 | |||
| 98461ff667 | |||
| 31c609daec | |||
| 7f9975cf1c | |||
| bfd904bfc8 | |||
| f46e50b271 | |||
| d8393a06dd | |||
| 5151bedf06 | |||
| c96f8259c1 | |||
| 82625fc07a | |||
| c93eed6384 | |||
| 5218323831 | |||
| 7e1705274c | |||
| 40db029887 | |||
| 8d637a434c | |||
| 712dc245ac | |||
| 47948e3712 | |||
| 570b0086b6 | |||
| 4cdf9207f3 | |||
| 6328ca512f | |||
| d7a0dbc6b1 | |||
| 525ced295c | |||
| 9c4d1594ac | |||
| fcffcfbacb | |||
| 59df649349 | |||
| 93e022cee5 | |||
| 710e00e02d | |||
| 4ef46e441c | |||
| bcc4d81d19 | |||
| 6c559e2338 | |||
| c0c7ad10ca | |||
| 10392ad344 | |||
| 34c14b7dd2 | |||
| 57272f84b1 | |||
| 9abb165f7b | |||
| 92543da28c | |||
| 81d03cb524 | |||
| edd988be60 | |||
| 6869dbe695 | |||
| d9828e2571 | |||
| 8fdb61aabf | |||
| b4455af306 | |||
| 56f394cefd | |||
| a1dc7a8f55 | |||
| 62b49ab14a | |||
| 375b8522fc | |||
| 2216856e3c | |||
| 468db8730a | |||
| aba23feddd | |||
| 44691a4ce6 | |||
| 22a560a990 | |||
| 0152759dfb | |||
| 37c5397319 | |||
| c564464e6b | |||
| 532497b3d1 | |||
| ab982b8e32 | |||
| 5be2a9d5e2 | |||
| f028913b0e | |||
| c8ad831039 | |||
| 3bf8fec2f1 | |||
| b9f6283a8e | |||
| 4c43c69d35 | |||
| fd64d34082 | |||
| d0da1f5fc9 | |||
| 7681c595b5 | |||
| 2bf226e918 | |||
| dd91b0a68b | |||
| fa8cea170a | |||
| 6e4236bf76 | |||
| 2c4f61eca5 | |||
| 23467cdbdb | |||
| f371ecb948 | |||
| 9f597ab7c0 | |||
| c9b39c379b | |||
| b08633daa3 | |||
| 5f5e07e6cc | |||
| 8af1439697 | |||
| 0fbc13aa4b | |||
| e232c1d5ba | |||
| e7a88f225d | |||
| 47e8822467 | |||
| 44805f4d58 | |||
| bba0f2e557 | |||
| b1fb68bb06 | |||
| fd2bfc384d | |||
| bcc1f25c53 | |||
| c442f8ae8c | |||
| 951596ffe4 | |||
| 1bfd575473 | |||
| e48f61bdc7 | |||
| 938c850ed8 | |||
| 55b6f848fc | |||
| 889a6c21ac | |||
| ecd95ed6b3 | |||
| 4e6ca95db9 | |||
| 52c1599240 | |||
| 9c392b2d44 | |||
| 42bac72be2 | |||
| 1195072aae | |||
| 09f0672734 | |||
| 00cd4b6a19 | |||
| c434a7e682 | |||
| 3f16ed5a9a | |||
| d5abb46fad | |||
| c56d0ce6d2 | |||
| cf95b631cd | |||
| 2993800e07 | |||
| e678b231a3 | |||
| a815fa37e7 | |||
| 81dcef070f | |||
| 690867e433 | |||
| 9954b03ced | |||
| ec01b1f29f | |||
| bfa4140593 | |||
| b14a4f5ebc | |||
| 29852e9b08 | |||
| 509444d059 | |||
| bbcc906133 | |||
| 22b7d86574 | |||
| e94ce7e2fd | |||
| afba59a953 | |||
| ec0acc486e | |||
| 0fcda10082 | |||
| 1f31443ed1 | |||
| a34e375193 | |||
| 899b6f72cb | |||
| db5b49b2ca | |||
| f2a47dec54 | |||
| 92c99d7873 | |||
| 2c754d71e3 | |||
| bcfd50b45b | |||
| 853ae56e7e | |||
| f41db212ec | |||
| b376df7691 | |||
| b88d9c6353 | |||
| ec9e548bd6 | |||
| 4f468a9f19 | |||
| 99b43e1302 | |||
| 6b34f38ce0 | |||
| 143e5c3140 | |||
| c9cc05bc3d | |||
| 6be59628c3 | |||
| 56b909c8e1 | |||
| 2f64307c19 | |||
| 7a68997b18 | |||
| 2d406ffc75 | |||
| 0534ca74d4 | |||
| e0fcbc39f6 | |||
| 6dca8270bc | |||
| d746eb7b22 | |||
| 930f39a0cd | |||
| 8f0306f499 | |||
| f3abfb8e66 | |||
| f8678f9e9d | |||
| 72114464b8 | |||
| 407373dcd6 | |||
| 6756a8b8a9 | |||
| d54cdd8802 | |||
| eddd13d70e | |||
| 13a65ad94f | |||
| 0cae7caaab | |||
| d2b8ff5deb | |||
| 0b10ba2ca9 | |||
| 0559040c0f | |||
| 8c0c156cbd | |||
| e76dda37ad | |||
| efb57e1cef | |||
| 79e72a94fe | |||
| 726d2c4274 | |||
| 678e947f21 | |||
| bbf7c6ed36 | |||
| 0403375799 | |||
| 1a6e4a119e | |||
| 4df8dbaa0d | |||
| d2f87d15ed | |||
| f849c85608 | |||
| 6391c4c0f1 | |||
| 6b576dc572 | |||
| f5dca8b7f7 | |||
| dbc0d9c8e2 | |||
| 55f5c0936a | |||
| 1407327a14 | |||
| cf9b6ec1f6 | |||
| bb642b1948 | |||
| 20ad40092c | |||
| a0079785af | |||
| 459de383b2 | |||
| 88272cba8b | |||
| 4e5c1fce8d | |||
| 986b9fe7e9 | |||
| a015ea203f | |||
| d0b505a577 | |||
| dbcfcd45f5 | |||
| a980c0cec9 | |||
| 80fa4a310b | |||
| a1ebb6f275 | |||
| f098e07c19 | |||
| ebf5437e52 | |||
| c999b7e354 | |||
| 1634d5fcca | |||
| d5d150449d | |||
| c0794215d3 | |||
| bee5b638dd | |||
| b7a87190d7 | |||
| 2cc0c1a808 | |||
| 079526fd3b | |||
| 12f584fff8 | |||
| c4c81e3d3b | |||
| 4a982ffd7b | |||
| 05022434eb | |||
| f7ff26ba65 | |||
| 2e12a67198 | |||
| 7353bda8ff | |||
| e28a043f3d | |||
| 0d9c1fba1d | |||
| 8c283127d5 | |||
| 97475071e1 | |||
| b5af662477 | |||
| 5cbab75c7d | |||
| 7470133a13 | |||
| c1de070a5e | |||
| 5b70bb4029 | |||
| 1145f25ee3 | |||
| 00ecfdeb06 | |||
| 12720a9fa7 | |||
| f11d79a931 | |||
| 35aeb3f420 | |||
| 2135dbb6a4 | |||
| a96cc2f148 | |||
| 73466b626d | |||
| 087c192e51 | |||
| 5a05fb0dd0 | |||
| 1f70a27e9c | |||
| b416508ef1 | |||
| 4f91037af4 | |||
| e5bbf9d728 | |||
| 811a2e90d9 | |||
| 21826010ef | |||
| c15774ce68 | |||
| bc7c7fe466 | |||
| 1f9564223b | |||
| 8a5e692373 | |||
| c99539b991 | |||
| eaadc6d93d | |||
| e89012efa8 | |||
| 8810ef2f40 | |||
| cf20697450 | |||
| f656f143c6 | |||
| 2e8c3a09d5 | |||
| 0d460bbf77 | |||
| 68035591da | |||
| 557dba4a32 | |||
| 2ccb9839d3 | |||
| b2c3523ecd | |||
| 1be38d8fa0 | |||
| 67f7dae4e3 | |||
| aba2a9932c | |||
| 181898cb5d | |||
| 8969a232ec | |||
| 59182a9fa0 | |||
| 39af7115d1 | |||
| ad21c10988 | |||
| 0308267071 | |||
| d892cde0b0 | |||
| 538e6cd19a | |||
| 5504f933e1 | |||
| 6be054513b | |||
| ba85aa49c7 | |||
| 1291ec78a5 | |||
| 1819ffe5bd | |||
| f9f4b2a260 | |||
| 095351bf39 | |||
| 8f4321b1ca | |||
| 78744cfbca | |||
| 5b2176770e | |||
| 8cc787f304 | |||
| 03e3c59bf0 | |||
| 3ceefe8732 | |||
| 514767c88b | |||
| db99fc70b6 | |||
| 32abe905f1 | |||
| b1b20e50ff | |||
| de2903b101 | |||
| ff705ec286 | |||
| 30b13c6357 | |||
| b9b6fe1f73 | |||
| a3a0e981ee | |||
| 0e3a0d7dc5 | |||
| ef70c17936 | |||
| 69b2d77df5 | |||
| bd0f085ae8 | |||
| 50e7c88aa4 | |||
| 0d03f4ea9d | |||
| a31830a3cb | |||
| 5032b27011 | |||
| 76182dbfbf | |||
| 7d77f0287d | |||
| ae20aee837 | |||
| de5a31c3b6 | |||
| da58b93026 | |||
| 8b40e000d6 | |||
| 54744958fa | |||
| 94187114e9 | |||
| 02f67d3c57 | |||
| ef9469a417 | |||
| 1eaf21f6a2 | |||
| 471cbcd450 | |||
| 2b05cf3bb4 | |||
| f0bbfe5f0a | |||
| 097b5c734f | |||
| c8dfab9ec6 | |||
| f6f2fff724 | |||
| 9c71d3dd64 | |||
| c244cc86dd | |||
| 7e56cc6515 | |||
| 78142eab55 | |||
| 1788f04f5c | |||
| 009c24a4b8 | |||
| 4693137b82 | |||
| 6e26c88c2c | |||
| d33defde68 | |||
| d30829116e | |||
| 6a9c2859b7 | |||
| 6b4c12eb04 | |||
| c031f538df | |||
| 127d9205b8 | |||
| 13d39ef746 | |||
| 885af07ddb | |||
| fe15b04f33 | |||
| d2b5b4f129 | |||
| a027010097 | |||
| ee1b395c5c | |||
| 81904005fc | |||
| dfd622e096 | |||
| 968e938949 | |||
| 77f795192e | |||
| dd23c77469 | |||
| 7239c5cd9a | |||
| d8dbe6271f | |||
| c1b8708ef5 | |||
| 0e284457be | |||
| 20d0de74e1 | |||
| 69da43eb70 | |||
| a7bfab4d96 | |||
| 5634be70f1 | |||
| 036f3a7f9c | |||
| 918ab3c2ec | |||
| af52b04e6f | |||
| 604a2335dd | |||
| 0bc0d23cb3 | |||
| aea6230bcc | |||
| 107537c341 | |||
| 34cd554a33 | |||
| 4aef2fa5d5 | |||
| c38247ed5c | |||
| 35a841f71a | |||
| e088c06a9a | |||
| deda119bea | |||
| 2aa25ba87b | |||
| cda4280d74 | |||
| cb43bb9ca4 | |||
| 45d494a8d8 | |||
| c31e37b30e | |||
| cae4cacd6e | |||
| 3885f7b6ae | |||
| e7a4142b2a | |||
| b49410e1d0 | |||
| 931d9fae4c | |||
| e133acd299 | |||
| cfea46e9d8 | |||
| 70900799d9 | |||
| af522f21fb | |||
| 6bb944f942 | |||
| 987f08a619 | |||
| 5cc23f0cd8 | |||
| 04e6c1d449 | |||
| 645133d9dd | |||
| 876d091602 | |||
| af99989c91 | |||
| 4c38227256 | |||
| 4b2c990368 | |||
| 3f26f9615c | |||
| 7c667e100e | |||
| 5ccf46b5a0 | |||
| e6e60861e5 | |||
| d2ae869bb4 | |||
| 7395ab63ab | |||
| dfd0c06153 | |||
| de53b30e69 | |||
| 50b5c4303e | |||
| 2782af3f57 | |||
| abddd2b6ee | |||
| 93ec574f68 | |||
| 044c3a0e14 | |||
| 5c2bb990d8 | |||
| b5dd6f9ea6 | |||
| c9bd3ecf6a | |||
| cb255a1619 | |||
| 4d07cb7dba | |||
| edb346d4ed | |||
| d12946ca8c | |||
| 977149af30 | |||
| 39c35e7d61 | |||
| 5e1651954d | |||
| f11322c710 | |||
| 315d786063 | |||
| 49dfe36cea | |||
| 82b6a2f5d1 | |||
| 0a7f5be81f | |||
| 6a581a695f | |||
| 997136ae25 | |||
| c29e44848b | |||
| 93820f833f | |||
| 70938eda17 | |||
| 5bac855697 | |||
| 4fd1113905 | |||
| 39e7213fc4 | |||
| ee06dd33df | |||
| c7c709fb3c | |||
| f331e5d5b6 | |||
| ef2437ba77 | |||
| c0a3122f5f | |||
| 17044876f6 | |||
| 18bb04c80a | |||
| a167f28ead | |||
| 96707b588e | |||
| ab1b74802d | |||
| 001009d350 | |||
| 45213103f6 | |||
| d0c781a549 | |||
| da22deac17 | |||
| 421f989faf | |||
| 175fc3bf03 | |||
| 6f91cfa9a3 | |||
| 5e5f2dffc0 | |||
| ea844857a2 | |||
| 3529595e6c | |||
| ea21f0fa03 | |||
| af6a792caa | |||
| 6a9fb6d2ea | |||
| eb13b2a7a1 | |||
| f80f7b445c | |||
| 6868cbe7b7 | |||
| f2bddfe054 | |||
| c9e1265de0 | |||
| 2a0e69d833 | |||
| 9037f25df7 | |||
| 71ad272a1b | |||
| 648ab84b21 | |||
| a63e67823d | |||
| a0fbe000ee | |||
| f00cf42f69 | |||
| 10c6af7e49 | |||
| fe5bb6694e | |||
| 2fc857092e | |||
| 1fc1c2b4e4 | |||
| 03bcbcc10d | |||
| ecdea602a3 | |||
| cfc63d49ec | |||
| a2faf34df8 | |||
| dd79e9b84a | |||
| 600151cc2c | |||
| c35aebe109 | |||
| fb7a34dca4 | |||
| 4b60cba66b | |||
| 92bb34fada | |||
| 58f6828833 | |||
| 4b400f8c7d |
@@ -0,0 +1,30 @@
|
||||
description = "Find relevant documentation and output GitHub URLs."
|
||||
|
||||
prompt = """
|
||||
## Mission: Find Relevant Documentation
|
||||
|
||||
Your task is to find documentation files relevant to the user's question within the current git repository and provide a list of GitHub URLs to view them.
|
||||
|
||||
### Workflow:
|
||||
|
||||
1. **Identify Repository Details**:
|
||||
* You may use shell commands like `git` or `gh` to get the remote URL of the repository.
|
||||
* From the remote URL, parse and construct the base GitHub URL (e.g., `https://github.com/user/repo`). You must handle both HTTPS (`https://github.com/user/repo.git`) and SSH (`git@github.com:user/repo.git`) formats.
|
||||
* Determine the default branch name. You can assume `main` for this purpose, as it is the most common.
|
||||
|
||||
2. **Search for Documentation**:
|
||||
* First, perform a targeted search across the repository for documentation files (e.g., `.md`, `.mdx`) that seem directly related to the user's question.
|
||||
* If this initial search yields no relevant results, and a `docs/` directory exists, read the content of all files within the `docs/` directory to find relevant information.
|
||||
* If you still can't find a direct match, broaden your search to include related concepts and synonyms of the keywords in the user's question.
|
||||
* For each file you identify as potentially relevant, read its content to confirm it addresses the user's query.
|
||||
|
||||
3. **Construct and Output URLs**:
|
||||
* For each file you identify as relevant, construct the full GitHub URL by combining the base URL, branch, and file path. **Do not use shell commands for this step.**
|
||||
* The URL format should be: `{BASE_GITHUB_URL}/blob/{BRANCH_NAME}/{PATH_TO_FILE_FROM_REPO_ROOT}`.
|
||||
* Present the final list to the user as a markdown list. Each item in the list should be the URL to the document, followed by a short summary of its content.
|
||||
* If, after all search attempts, you cannot find any relevant documentation, ask the user clarifying questions to better understand their needs. Do not return any URLs in this case.
|
||||
|
||||
### QUESTION:
|
||||
|
||||
{{args}}
|
||||
"""
|
||||
@@ -0,0 +1,13 @@
|
||||
description = "Go back to main and clean up the branch."
|
||||
|
||||
prompt = """
|
||||
I'm done with the work on this branch, and I'm ready to go back to main and clean up.
|
||||
|
||||
Here is the workflow I'd like you to follow:
|
||||
|
||||
1. **Get Current Branch:** First, I need you to get the name of the current branch and save it.
|
||||
2. **Branch Check:** Check if the current branch is `main`. If it is, I need you to stop and let me know.
|
||||
3. **Go to Main:** Next, I need you to checkout the main branch.
|
||||
4. **Pull Latest:** Once you are on the main branch, I need you to pull down the latest changes to make sure I'm up to date.
|
||||
5. **Branch Cleanup:** Finally, I need you to delete the branch that you noted in the first step.
|
||||
"""
|
||||
@@ -0,0 +1,47 @@
|
||||
description = "Review a specific pull request"
|
||||
|
||||
prompt = """
|
||||
## Mission: Comprehensive Pull Request Review
|
||||
|
||||
Today, our mission is to meticulously review community pull requests (PRs) for this project. We will proceed systematically, evaluating each candidate PR for its quality, adherence to standards, and readiness for merging.
|
||||
|
||||
### Workflow:
|
||||
|
||||
1. **PR Preparation & Initial Assessment**:
|
||||
* **You will check out the designated PR {{args}}** into a temporary branch.
|
||||
* **Execute the preflight checks (`npm run preflight`)**. This includes building, linting, and running all unit tests.
|
||||
* Analyze the output of these preflight checks, noting any failures, warnings, or linting issues.
|
||||
|
||||
2. **In-Depth Code Review**:
|
||||
* **Your primary role is to conduct a thorough and in-depth code review** of the changes introduced in the PR. Focus your analysis on the following criteria:
|
||||
* **Correctness**: Does the code achieve its stated purpose without bugs or logical errors?
|
||||
* **Maintainability**: Is the code clean, well-structured, and easy to understand and modify in the future? Consider factors like code clarity, modularity, and adherence to established design patterns.
|
||||
* **Readability**: Is the code well-commented (where necessary) and consistently formatted according to our project's coding style guidelines?
|
||||
* **Efficiency**: Are there any obvious performance bottlenecks or resource inefficiencies introduced by the changes?
|
||||
* **Security**: Are there any potential security vulnerabilities or insecure coding practices?
|
||||
* **Edge Cases and Error Handling**: Does the code appropriately handle edge cases and potential errors?
|
||||
* **Testability**: Is the new or modified code adequately covered by tests (even if preflight checks pass)? Suggest additional test cases that would improve coverage or robustness.
|
||||
* Based on your analysis, you will determine if the PR is **safe to merge**.
|
||||
|
||||
3. **Reviewing Previous Feedback**:
|
||||
* **Access and examine the PR's history** to identify any **outstanding requests or unresolved comments from previous reviews**. Incorporate these into your current review and explicitly highlight if they have been adequately addressed in the current state of the PR.
|
||||
|
||||
4. **Decision and Output Generation**:
|
||||
* **If the PR is deemed safe to merge** (after your comprehensive review and considering previous feedback):
|
||||
* Draft a **friendly, concise, and professional approval message**.
|
||||
* **The approval message should:**
|
||||
* Clearly state that the PR is approved.
|
||||
* Briefly acknowledge the quality or value of the contribution (e.g., "Great work on X feature!" or "Appreciate the fix for Y issue!").
|
||||
* **Do NOT mention the preflight checks or unit testing**, as these are internal processes.
|
||||
* Be suitable for public display on GitHub.
|
||||
* **If the PR is NOT safe to merge**:
|
||||
* Provide a **clear, constructive, and detailed summary of the issues found**.
|
||||
* Suggest **specific actionable changes** required for the PR to become merge-ready.
|
||||
* Ensure the feedback is professional and encourages the contributor.
|
||||
|
||||
### Post-PR Action:
|
||||
|
||||
* After providing your review and decision for the current PR, I will wait for you to perform any manual testing you wish to do. Please let me know when you are finished.
|
||||
* Once you have confirmed that you are done, I will switch to the `main` branch, clean up the local branch, and perform a pull to ensure we are synchronized with the latest upstream changes for the next review.
|
||||
|
||||
"""
|
||||
+11
-5
@@ -1,7 +1,13 @@
|
||||
# By default, require reviews from the release approvers for all files.
|
||||
* @google-gemini/gemini-cli-askmode-approvers
|
||||
# By default, require reviews from the maintainers for all files.
|
||||
* @google-gemini/gemini-cli-maintainers
|
||||
|
||||
# The following files don't need reviews from the release approvers.
|
||||
# Require reviews from the release approvers for critical files.
|
||||
# These patterns override the rule above.
|
||||
**/*.md
|
||||
/docs/
|
||||
/package.json @google-gemini/gemini-cli-askmode-approvers
|
||||
/package-lock.json @google-gemini/gemini-cli-askmode-approvers
|
||||
/GEMINI.md @google-gemini/gemini-cli-askmode-approvers
|
||||
/SECURITY.md @google-gemini/gemini-cli-askmode-approvers
|
||||
/LICENSE @google-gemini/gemini-cli-askmode-approvers
|
||||
/.github/workflows/ @google-gemini/gemini-cli-askmode-approvers
|
||||
/packages/cli/package.json @google-gemini/gemini-cli-askmode-approvers
|
||||
/packages/core/package.json @google-gemini/gemini-cli-askmode-approvers
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: 'Create and Merge Pull Request'
|
||||
description: 'Creates a pull request and merges it automatically.'
|
||||
|
||||
inputs:
|
||||
branch-name:
|
||||
description: 'The name of the branch to create the PR from.'
|
||||
required: true
|
||||
pr-title:
|
||||
description: 'The title of the pull request.'
|
||||
required: true
|
||||
pr-body:
|
||||
description: 'The body of the pull request.'
|
||||
required: true
|
||||
base-branch:
|
||||
description: 'The branch to merge into.'
|
||||
required: true
|
||||
default: 'main'
|
||||
github-token:
|
||||
description: 'The GitHub token to use for creating the pull request.'
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: false
|
||||
default: 'false'
|
||||
working-directory:
|
||||
description: 'The working directory to run the commands in.'
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Create and Approve Pull Request'
|
||||
if: "inputs.dry-run == 'false'"
|
||||
env:
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
set -e
|
||||
if ! git ls-remote --exit-code --heads origin "${{ inputs.branch-name }}"; then
|
||||
echo "::error::Branch '${{ inputs.branch-name }}' does not exist on the remote repository."
|
||||
exit 1
|
||||
fi
|
||||
PR_URL=$(gh pr create \
|
||||
--title "${{ inputs.pr-title }}" \
|
||||
--body "${{ inputs.pr-body }}" \
|
||||
--base "${{ inputs.base-branch }}" \
|
||||
--head "${{ inputs.branch-name }}" \
|
||||
--fill)
|
||||
gh pr merge "$PR_URL" --auto --squash
|
||||
@@ -0,0 +1,176 @@
|
||||
name: 'Publish Release'
|
||||
description: 'Builds, prepares, and publishes the gemini-cli packages to npm and creates a GitHub release.'
|
||||
|
||||
inputs:
|
||||
release-version:
|
||||
description: 'The version to release (e.g., 0.1.11).'
|
||||
required: true
|
||||
npm-tag:
|
||||
description: 'The npm tag to publish with (e.g., latest, preview, nightly).'
|
||||
required: true
|
||||
wombat-token-core:
|
||||
description: 'The npm token for the @google/gemini-cli-core package.'
|
||||
required: true
|
||||
wombat-token-cli:
|
||||
description: 'The npm token for the @google/gemini-cli package.'
|
||||
required: true
|
||||
github-token:
|
||||
description: 'The GitHub token for creating the release.'
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
type: 'string'
|
||||
required: true
|
||||
release-tag:
|
||||
description: 'The release tag for the release (e.g., v0.1.11).'
|
||||
required: true
|
||||
previous-tag:
|
||||
description: 'The previous tag to use for generating release notes.'
|
||||
required: true
|
||||
skip-github-release:
|
||||
description: 'Whether to skip creating a GitHub release.'
|
||||
type: 'boolean'
|
||||
required: false
|
||||
default: false
|
||||
working-directory:
|
||||
description: 'The working directory to run the steps in.'
|
||||
required: false
|
||||
default: '.'
|
||||
force-skip-tests:
|
||||
description: 'Skip tests and validation'
|
||||
required: false
|
||||
default: false
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: '👤 Configure Git User'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
git config user.name "gemini-cli-robot"
|
||||
git config user.email "gemini-cli-robot@google.com"
|
||||
|
||||
- name: '🌿 Create and switch to a release branch'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
id: 'release_branch'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
BRANCH_NAME="release/${{ inputs.release-tag }}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: '⬆️ Update package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm run release:version "${{ inputs.release-version }}"
|
||||
|
||||
- name: '💾 Commit and Conditionally Push package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
DRY_RUN: '${{ inputs.dry-run }}'
|
||||
RELEASE_TAG: '${{ inputs.release-tag }}'
|
||||
run: |-
|
||||
git add package.json package-lock.json packages/*/package.json
|
||||
git commit -m "chore(release): ${RELEASE_TAG}"
|
||||
if [[ "${DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
|
||||
else
|
||||
echo "Dry run enabled. Skipping push."
|
||||
fi
|
||||
|
||||
- name: '🛠️ Build and Prepare Packages'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm run build:packages
|
||||
npm run prepare:package
|
||||
|
||||
- name: 'Configure npm for publishing'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '${{ inputs.working-directory }}/.nvmrc'
|
||||
registry-url: 'https://wombat-dressing-room.appspot.com'
|
||||
scope: '@google'
|
||||
|
||||
- name: '📦 Publish @google/gemini-cli-core'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ inputs.wombat-token-core }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm publish \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="@google/gemini-cli-core" \
|
||||
--no-tag
|
||||
|
||||
- name: '🔗 Install latest core package'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: "${{ inputs.dry-run == 'false' }}"
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm install "@google/gemini-cli-core@${{ inputs.release-version }}" \
|
||||
--workspace="@google/gemini-cli" \
|
||||
--save-exact
|
||||
|
||||
- name: '📦 Publish @google/gemini-cli'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ inputs.wombat-token-cli }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm publish \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="@google/gemini-cli" \
|
||||
--no-tag
|
||||
|
||||
- name: '🔬 Verify NPM release by version'
|
||||
uses: './.github/actions/verify-release'
|
||||
if: "${{ inputs.dry-run == 'false' && inputs.force-skip-tests == 'false' }}"
|
||||
with:
|
||||
npm-package: '@google/gemini-cli@${{ inputs.release-version }}'
|
||||
expected-version: '${{ inputs.release-version }}'
|
||||
ref: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
|
||||
- name: '🏷️ Tag release'
|
||||
uses: './.github/actions/tag-npm-release'
|
||||
if: "${{ inputs.dry-run == 'false' }}"
|
||||
with:
|
||||
channel: '${{ inputs.npm-tag }}'
|
||||
version: '${{ inputs.release-version }}'
|
||||
dry-run: '${{ inputs.dry-run }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
|
||||
- name: '🎁 Bundle'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm run bundle
|
||||
|
||||
- name: '🎉 Create GitHub Release'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: "${{ inputs.dry-run == 'false' && inputs.skip-github-release == 'false' && inputs.npm-tag != 'dev' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-token }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
gh release create "${{ inputs.release-tag }}" \
|
||||
bundle/gemini.js \
|
||||
--target "${{ steps.release_branch.outputs.BRANCH_NAME }}" \
|
||||
--title "Release ${{ inputs.release-tag }}" \
|
||||
--notes-start-tag "${{ inputs.previous-tag }}" \
|
||||
--generate-notes
|
||||
|
||||
- name: '🧹 Clean up release branch'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: "${{ inputs.dry-run == 'false' }}"
|
||||
continue-on-error: true
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "Cleaning up release branch ${{ steps.release_branch.outputs.BRANCH_NAME }}..."
|
||||
git push origin --delete "${{ steps.release_branch.outputs.BRANCH_NAME }}"
|
||||
@@ -0,0 +1,73 @@
|
||||
name: 'Push to docker'
|
||||
description: 'Builds packages and pushes a docker image to GHCR'
|
||||
|
||||
inputs:
|
||||
github-actor:
|
||||
description: 'Github actor'
|
||||
required: true
|
||||
github-secret:
|
||||
description: 'Github secret'
|
||||
required: true
|
||||
ref-name:
|
||||
description: 'Github ref name'
|
||||
required: true
|
||||
github-sha:
|
||||
description: 'Github Commit SHA Hash'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
ref: '${{ inputs.github-sha }}'
|
||||
fetch-depth: 0
|
||||
- name: 'Install Dependencies'
|
||||
shell: 'bash'
|
||||
run: 'npm install'
|
||||
- name: 'Set up Docker Buildx'
|
||||
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
|
||||
- name: 'build'
|
||||
shell: 'bash'
|
||||
run: 'npm run build'
|
||||
- name: 'pack @google/gemini-cli'
|
||||
shell: 'bash'
|
||||
run: 'npm pack -w @google/gemini-cli --pack-destination ./packages/cli/dist'
|
||||
- name: 'pack @google/gemini-cli-core'
|
||||
shell: 'bash'
|
||||
run: 'npm pack -w @google/gemini-cli-core --pack-destination ./packages/core/dist'
|
||||
- name: 'Log in to GitHub Container Registry'
|
||||
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
|
||||
with:
|
||||
registry: 'ghcr.io'
|
||||
username: '${{ inputs.github-actor }}'
|
||||
password: '${{ inputs.github-secret }}'
|
||||
- name: 'Get branch name'
|
||||
id: 'branch_name'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
REF_NAME="${{ inputs.ref-name }}"
|
||||
echo "name=${REF_NAME%/merge}" >> $GITHUB_OUTPUT
|
||||
- name: 'Build and Push the Docker Image'
|
||||
uses: 'docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83' # ratchet:docker/build-push-action@v6
|
||||
with:
|
||||
context: '.'
|
||||
file: './Dockerfile'
|
||||
push: true
|
||||
provenance: false # avoid pushing 3 images to Aritfact Registry
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}/cli:${{ steps.branch_name.outputs.name }}
|
||||
ghcr.io/${{ github.repository }}/cli:${{ inputs.github-sha }}
|
||||
- name: 'Create issue on failure'
|
||||
if: |-
|
||||
${{ failure() }}
|
||||
shell: 'bash'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-secret }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |-
|
||||
gh issue create \
|
||||
--title "Docker build failed" \
|
||||
--body "The docker build failed. See the full run for details: ${DETAILS_URL}" \
|
||||
--label "kind/bug,release-failure"
|
||||
@@ -0,0 +1,91 @@
|
||||
name: 'Build and push sandbox docker'
|
||||
description: 'Pushes sandbox docker image to container registry'
|
||||
|
||||
inputs:
|
||||
github-actor:
|
||||
description: 'Github actor'
|
||||
required: true
|
||||
github-secret:
|
||||
description: 'Github secret'
|
||||
required: true
|
||||
dockerhub-username:
|
||||
description: 'Dockerhub username'
|
||||
required: true
|
||||
dockerhub-token:
|
||||
description: 'Dockerhub PAT w/ R+W'
|
||||
required: true
|
||||
github-sha:
|
||||
description: 'Github Commit SHA Hash'
|
||||
required: true
|
||||
github-ref-name:
|
||||
description: 'Github ref name'
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Whether this is a dry run.'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
ref: '${{ inputs.github-sha }}'
|
||||
fetch-depth: 0
|
||||
- name: 'Install Dependencies'
|
||||
shell: 'bash'
|
||||
run: 'npm install'
|
||||
- name: 'npm build'
|
||||
shell: 'bash'
|
||||
run: 'npm run build'
|
||||
- name: 'Set up Docker Buildx'
|
||||
uses: 'docker/setup-buildx-action@v3'
|
||||
- name: 'Log in to GitHub Container Registry'
|
||||
uses: 'docker/login-action@v3'
|
||||
with:
|
||||
registry: 'docker.io'
|
||||
username: '${{ inputs.dockerhub-username }}'
|
||||
password: '${{ inputs.dockerhub-token }}'
|
||||
- name: 'determine image tag'
|
||||
id: 'image_tag'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
SHELL_TAG_NAME="${{ inputs.github-ref-name }}"
|
||||
FINAL_TAG="${{ inputs.github-sha }}"
|
||||
if [[ "$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
|
||||
echo "Release detected."
|
||||
FINAL_TAG="${SHELL_TAG_NAME#v}"
|
||||
else
|
||||
echo "Development release detected. Using commit SHA as tag."
|
||||
fi
|
||||
echo "Determined image tag: $FINAL_TAG"
|
||||
echo "FINAL_TAG=$FINAL_TAG" >> $GITHUB_OUTPUT
|
||||
- name: 'build'
|
||||
id: 'docker_build'
|
||||
shell: 'bash'
|
||||
env:
|
||||
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
GEMINI_SANDBOX: 'docker'
|
||||
run: |-
|
||||
npm run build:sandbox -- \
|
||||
--image google/gemini-cli-sandbox:${{ steps.image_tag.outputs.FINAL_TAG }} \
|
||||
--output-file final_image_uri.txt
|
||||
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
|
||||
- name: 'publish'
|
||||
shell: 'bash'
|
||||
if: "${{ inputs.dry-run == 'false' }}"
|
||||
run: |-
|
||||
docker push "${{ steps.docker_build.outputs.uri }}"
|
||||
- name: 'Create issue on failure'
|
||||
if: |-
|
||||
${{ failure() }}
|
||||
shell: 'bash'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-secret }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |-
|
||||
gh issue create \
|
||||
--title "Docker build failed" \
|
||||
--body "The docker build failed. See the full run for details: ${DETAILS_URL}" \
|
||||
--label "kind/bug,release-failure"
|
||||
@@ -0,0 +1,33 @@
|
||||
name: 'Run Tests'
|
||||
description: 'Runs the preflight checks and integration tests.'
|
||||
|
||||
inputs:
|
||||
gemini_api_key:
|
||||
description: 'The API key for running integration tests.'
|
||||
required: true
|
||||
working-directory:
|
||||
description: 'The working directory to run the tests in.'
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Run Tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
echo "::group::Build"
|
||||
npm run build
|
||||
echo "::endgroup::"
|
||||
echo "::group::Unit Tests"
|
||||
npm run test:ci
|
||||
echo "::endgroup::"
|
||||
echo "::group::Integration Tests (no sandbox)"
|
||||
npm run test:integration:sandbox:none
|
||||
echo "::endgroup::"
|
||||
echo "::group::Integration Tests (docker sandbox)"
|
||||
npm run test:integration:sandbox:docker
|
||||
echo "::endgroup::"
|
||||
shell: 'bash'
|
||||
@@ -0,0 +1,53 @@
|
||||
name: 'Tag an NPM release'
|
||||
description: 'Tags a specific npm version to a specific channel.'
|
||||
|
||||
inputs:
|
||||
channel:
|
||||
description: 'NPM Channel tag'
|
||||
required: true
|
||||
version:
|
||||
description: 'version'
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: true
|
||||
wombat-token-core:
|
||||
description: 'The npm token for the wombat @google/gemini-cli-core'
|
||||
required: true
|
||||
wombat-token-cli:
|
||||
description: 'The npm token for wombat @google/gemini-cli'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: 'https://wombat-dressing-room.appspot.com'
|
||||
scope: '@google'
|
||||
|
||||
- name: 'Change tag for @google/gemini-cli-core'
|
||||
if: |-
|
||||
${{ inputs.dry-run == 'false' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ inputs.wombat-token-core }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm dist-tag add @google/gemini-cli-core@${{ inputs.version }} ${{ inputs.channel }}
|
||||
|
||||
- name: 'Change tag for @google/gemini-cli'
|
||||
if: |-
|
||||
${{ inputs.dry-run == 'false' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ inputs.wombat-token-cli }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm dist-tag add @google/gemini-cli@${{ inputs.version }} ${{ inputs.channel }}
|
||||
|
||||
- name: 'Log dry run'
|
||||
if: |-
|
||||
${{ inputs.dry-run == 'true' }}
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "Dry run: Would have added tag '${{ inputs.channel }}' to version '${{ inputs.version }}' for @google/gemini-cli and @google/gemini-cli-core."
|
||||
@@ -0,0 +1,57 @@
|
||||
name: 'Verify an NPM release'
|
||||
description: 'Fetches a package from NPM and does some basic smoke tests'
|
||||
|
||||
inputs:
|
||||
npm-package:
|
||||
description: 'NPM Package'
|
||||
required: true
|
||||
default: '@google/gemini-cli@latest'
|
||||
expected-version:
|
||||
description: 'Expected version'
|
||||
required: true
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: '📝 Print Inputs'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "${{ toJSON(inputs) }}"
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
path: 'verify'
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Install from NPM'
|
||||
uses: 'nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08' # ratchet:nick-fields/retry@v3
|
||||
with:
|
||||
timeout_seconds: 900
|
||||
retry_wait_seconds: 30
|
||||
max_attempts: 10
|
||||
command: 'cd ./verify && pkg="${{ inputs.npm-package }}" && npm install --prefer-online --no-cache -g "$pkg"'
|
||||
|
||||
# This provides a very basic smoke test for Gemini CLI
|
||||
- name: 'Run Gemini CLI'
|
||||
id: 'gemini_cli'
|
||||
shell: 'bash'
|
||||
working-directory: './verify'
|
||||
run: |-
|
||||
echo "gemini_version=$(gemini --version)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Force a failure if it doesn't match
|
||||
- name: 'Fail workflow if version does not match'
|
||||
if: '${{ steps.gemini_cli.outputs.gemini_version != inputs.expected-version }}'
|
||||
shell: 'bash'
|
||||
working-directory: './verify'
|
||||
run: |-
|
||||
echo '❌ Got ${{ steps.gemini_cli.outputs.gemini_version }} from ${{ inputs.npm-package }}'
|
||||
echo '❌ Expected Version ${{ inputs.expected-version }}'
|
||||
|
||||
exit 1
|
||||
@@ -19,24 +19,10 @@ process_pr() {
|
||||
local PR_NUMBER=$1
|
||||
echo "🔄 Processing PR #${PR_NUMBER}"
|
||||
|
||||
# Get PR body with error handling
|
||||
local PR_BODY
|
||||
if ! PR_BODY=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json body -q .body 2>/dev/null); then
|
||||
echo " ⚠️ Could not fetch PR #${PR_NUMBER} details"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Look for issue references using multiple patterns
|
||||
local ISSUE_NUMBER=""
|
||||
|
||||
# Pattern 1: Direct reference like #123
|
||||
if [[ -z "${ISSUE_NUMBER}" ]]; then
|
||||
ISSUE_NUMBER=$(echo "${PR_BODY}" | grep -oE '#[0-9]+' | head -1 | sed 's/#//' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Pattern 2: Closes/Fixes/Resolves patterns (case-insensitive)
|
||||
if [[ -z "${ISSUE_NUMBER}" ]]; then
|
||||
ISSUE_NUMBER=$(echo "${PR_BODY}" | grep -iE '(closes?|fixes?|resolves?) #[0-9]+' | grep -oE '#[0-9]+' | head -1 | sed 's/#//' 2>/dev/null || echo "")
|
||||
# Get closing issue number with error handling
|
||||
local ISSUE_NUMBER
|
||||
if ! ISSUE_NUMBER=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json closingIssuesReferences -q '.closingIssuesReferences.nodes[0].number' 2>/dev/null); then
|
||||
echo " ⚠️ Could not fetch closing issue for PR #${PR_NUMBER}"
|
||||
fi
|
||||
|
||||
if [[ -z "${ISSUE_NUMBER}" ]]; then
|
||||
@@ -95,21 +81,6 @@ process_pr() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Find labels to remove (on PR but not on issue)
|
||||
local LABELS_TO_REMOVE=""
|
||||
for label in "${PR_LABEL_ARRAY[@]}"; do
|
||||
if [[ -n "${label}" ]] && [[ " ${ISSUE_LABEL_ARRAY[*]} " != *" ${label} "* ]]; then
|
||||
# Don't remove status/need-issue since we already handled it
|
||||
if [[ "${label}" != "status/need-issue" ]]; then
|
||||
if [[ -z "${LABELS_TO_REMOVE}" ]]; then
|
||||
LABELS_TO_REMOVE="${label}"
|
||||
else
|
||||
LABELS_TO_REMOVE="${LABELS_TO_REMOVE},${label}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Apply label changes
|
||||
if [[ -n "${LABELS_TO_ADD}" ]]; then
|
||||
echo "➕ Adding labels: ${LABELS_TO_ADD}"
|
||||
@@ -118,14 +89,7 @@ process_pr() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${LABELS_TO_REMOVE}" ]]; then
|
||||
echo "➖ Removing labels: ${LABELS_TO_REMOVE}"
|
||||
if ! gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --remove-label "${LABELS_TO_REMOVE}" 2>/dev/null; then
|
||||
echo " ⚠️ Failed to remove some labels"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${LABELS_TO_ADD}" ]] && [[ -z "${LABELS_TO_REMOVE}" ]]; then
|
||||
if [[ -z "${LABELS_TO_ADD}" ]]; then
|
||||
echo "✅ Labels already synchronized"
|
||||
fi
|
||||
echo "needs_comment=false" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
+201
-246
@@ -1,4 +1,4 @@
|
||||
name: 'Gemini CLI CI'
|
||||
name: 'Testing: CI'
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -10,6 +10,13 @@ on:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_ref:
|
||||
description: 'Branch to run on'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: 'string'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
@@ -25,67 +32,15 @@ defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
env:
|
||||
ACTIONLINT_VERSION: '1.7.7'
|
||||
SHELLCHECK_VERSION: '0.11.0'
|
||||
YAMLLINT_VERSION: '1.35.1'
|
||||
|
||||
jobs:
|
||||
#
|
||||
# Lint: GitHub Actions
|
||||
#
|
||||
lint_github_actions:
|
||||
name: 'Lint (GitHub Actions)'
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 'Install shellcheck' # Actionlint uses shellcheck
|
||||
run: |-
|
||||
mkdir -p "${RUNNER_TEMP}/shellcheck"
|
||||
curl -sSLo "${RUNNER_TEMP}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
|
||||
tar -xf "${RUNNER_TEMP}/.shellcheck.txz" -C "${RUNNER_TEMP}/shellcheck" --strip-components=1
|
||||
echo "${RUNNER_TEMP}/shellcheck" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: 'Install actionlint'
|
||||
run: |-
|
||||
mkdir -p "${RUNNER_TEMP}/actionlint"
|
||||
curl -sSLo "${RUNNER_TEMP}/.actionlint.tgz" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
|
||||
tar -xzf "${RUNNER_TEMP}/.actionlint.tgz" -C "${RUNNER_TEMP}/actionlint"
|
||||
echo "${RUNNER_TEMP}/actionlint" >> "${GITHUB_PATH}"
|
||||
|
||||
# For actionlint, we specifically ignore shellcheck rules that are
|
||||
# annoying or unhelpful. See the shellcheck action for a description.
|
||||
- name: 'Run actionlint'
|
||||
run: |-
|
||||
actionlint \
|
||||
-color \
|
||||
-format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Filepath}}@{{$err.Line}} {{$err.Message}}%0A```%0A{{replace $err.Snippet "\\n" "%0A"}}%0A```\n{{end}}' \
|
||||
-ignore 'SC2002:' \
|
||||
-ignore 'SC2016:' \
|
||||
-ignore 'SC2129:' \
|
||||
-ignore 'label ".+" is unknown'
|
||||
|
||||
- name: 'Run ratchet'
|
||||
uses: 'sethvargo/ratchet@8b4ca256dbed184350608a3023620f267f0a5253' # ratchet:sethvargo/ratchet@v0.11.4
|
||||
with:
|
||||
files: |-
|
||||
.github/workflows/*.yml
|
||||
.github/actions/**/*.yml
|
||||
|
||||
#
|
||||
# Lint: Javascript
|
||||
#
|
||||
lint_javascript:
|
||||
name: 'Lint (Javascript)'
|
||||
runs-on: 'ubuntu-latest'
|
||||
lint:
|
||||
name: 'Lint'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
@@ -95,156 +50,32 @@ jobs:
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run formatter check'
|
||||
run: |-
|
||||
npm run format
|
||||
git diff --exit-code
|
||||
- name: 'Check lockfile'
|
||||
run: 'npm run check:lockfile'
|
||||
|
||||
- name: 'Run linter'
|
||||
run: |-
|
||||
npm run lint:ci
|
||||
- name: 'Install linters'
|
||||
run: 'node scripts/lint.js --setup'
|
||||
|
||||
- name: 'Run linter on integration tests'
|
||||
run: |-
|
||||
npx eslint integration-tests --max-warnings 0
|
||||
- name: 'Run ESLint'
|
||||
run: 'node scripts/lint.js --eslint'
|
||||
|
||||
- name: 'Run formatter on integration tests'
|
||||
run: |-
|
||||
npx prettier --check integration-tests
|
||||
git diff --exit-code
|
||||
- name: 'Run actionlint'
|
||||
run: 'node scripts/lint.js --actionlint'
|
||||
|
||||
- name: 'Build project'
|
||||
run: |-
|
||||
npm run build
|
||||
|
||||
- name: 'Run type check'
|
||||
run: |-
|
||||
npm run typecheck
|
||||
|
||||
#
|
||||
# Lint: Shell
|
||||
#
|
||||
lint_shell:
|
||||
name: 'Lint (Shell)'
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 'Install shellcheck'
|
||||
run: |-
|
||||
mkdir -p "${RUNNER_TEMP}/shellcheck"
|
||||
curl -sSLo "${RUNNER_TEMP}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
|
||||
tar -xf "${RUNNER_TEMP}/.shellcheck.txz" -C "${RUNNER_TEMP}/shellcheck" --strip-components=1
|
||||
echo "${RUNNER_TEMP}/shellcheck" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: 'Install shellcheck problem matcher'
|
||||
run: |-
|
||||
cat > "${RUNNER_TEMP}/shellcheck/problem-matcher-lint-shell.json" <<"EOF"
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "lint_shell",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.*):(\\\\d+):(\\\\d+):\\\\s+(?:fatal\\\\s+)?(warning|error):\\\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
echo "::add-matcher::${RUNNER_TEMP}/shellcheck/problem-matcher-lint-shell.json"
|
||||
|
||||
# Note that only warning and error severity show up in the github files
|
||||
# page. So we replace 'style' and 'note' with 'warning' to make it show
|
||||
# up.
|
||||
#
|
||||
# We also try and find all bash scripts even if they don't have an
|
||||
# explicit extension.
|
||||
#
|
||||
# We explicitly ignore the following rules:
|
||||
#
|
||||
# - SC2002: This rule suggests using "cmd < file" instead of "cat | cmd".
|
||||
# While < is more efficient, pipes are much more readable and expected.
|
||||
#
|
||||
# - SC2129: This rule suggests grouping multiple writes to a file in
|
||||
# braces like "{ cmd1; cmd2; } >> file". This is unexpected and less
|
||||
# readable.
|
||||
#
|
||||
# - SC2310: This is an optional warning that only appears with "set -e"
|
||||
# and when a command is used as a conditional.
|
||||
- name: 'Run shellcheck'
|
||||
run: |-
|
||||
git ls-files | grep -E '^([^.]+|.*\.(sh|zsh|bash))$' | xargs file --mime-type \
|
||||
| grep "text/x-shellscript" | awk '{ print substr($1, 1, length($1)-1) }' \
|
||||
| xargs shellcheck \
|
||||
--check-sourced \
|
||||
--enable=all \
|
||||
--exclude=SC2002,SC2129,SC2310 \
|
||||
--severity=style \
|
||||
--format=gcc \
|
||||
--color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g'
|
||||
|
||||
#
|
||||
# Lint: YAML
|
||||
#
|
||||
lint_yaml:
|
||||
name: 'Lint (YAML)'
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 'Setup Python'
|
||||
uses: 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065' # ratchet:actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3'
|
||||
|
||||
- name: 'Install yamllint'
|
||||
run: |-
|
||||
pip install --user "yamllint==${YAMLLINT_VERSION}"
|
||||
run: 'node scripts/lint.js --shellcheck'
|
||||
|
||||
- name: 'Run yamllint'
|
||||
run: |-
|
||||
git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github
|
||||
run: 'node scripts/lint.js --yamllint'
|
||||
|
||||
#
|
||||
# Lint: All
|
||||
#
|
||||
# This is a virtual job that other jobs depend on to wait for all linters to
|
||||
# finish. It's also used to ensure linting happens on CI via required
|
||||
# workflows.
|
||||
lint:
|
||||
name: 'Lint'
|
||||
needs:
|
||||
- 'lint_github_actions'
|
||||
- 'lint_javascript'
|
||||
- 'lint_shell'
|
||||
- 'lint_yaml'
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- run: |-
|
||||
echo 'All linters finished!'
|
||||
- name: 'Run Prettier'
|
||||
run: 'node scripts/lint.js --prettier'
|
||||
|
||||
#
|
||||
# Test: Node
|
||||
#
|
||||
test:
|
||||
name: 'Test'
|
||||
runs-on: '${{ matrix.os }}'
|
||||
test_linux:
|
||||
name: 'Test (Linux)'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs:
|
||||
- 'lint'
|
||||
permissions:
|
||||
@@ -252,12 +83,7 @@ jobs:
|
||||
checks: 'write'
|
||||
pull-requests: 'write'
|
||||
strategy:
|
||||
fail-fast: false # So we can see all test failures
|
||||
matrix:
|
||||
os:
|
||||
- 'macos-latest'
|
||||
- 'ubuntu-latest'
|
||||
- 'windows-latest'
|
||||
node-version:
|
||||
- '20.x'
|
||||
- '22.x'
|
||||
@@ -273,18 +99,19 @@ jobs:
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Build project'
|
||||
run: |-
|
||||
npm run build
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Install dependencies for testing'
|
||||
run: |-
|
||||
npm ci
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
NO_COLOR: true
|
||||
run: 'npm run test:ci'
|
||||
|
||||
- name: 'Wait for file system sync'
|
||||
run: 'sleep 2'
|
||||
|
||||
- name: 'Publish Test Report (for non-forks)'
|
||||
if: |-
|
||||
${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
@@ -300,7 +127,67 @@ jobs:
|
||||
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'test-results-fork-${{ matrix.node-version }}-${{ matrix.os }}'
|
||||
name: 'test-results-fork-${{ matrix.node-version }}-${{ runner.os }}'
|
||||
path: 'packages/*/junit.xml'
|
||||
|
||||
test_mac:
|
||||
name: 'Test (Mac)'
|
||||
runs-on: '${{ matrix.os }}'
|
||||
needs:
|
||||
- 'lint'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
pull-requests: 'write'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- 'macos-latest'
|
||||
node-version:
|
||||
- '20.x'
|
||||
- '22.x'
|
||||
- '24.x'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Install dependencies for testing'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
NO_COLOR: true
|
||||
run: 'npm run test:ci -- --coverage.enabled=false'
|
||||
|
||||
- name: 'Wait for file system sync'
|
||||
run: 'sleep 2'
|
||||
|
||||
- name: 'Publish Test Report (for non-forks)'
|
||||
if: |-
|
||||
${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
|
||||
with:
|
||||
name: 'Test Results (Node ${{ matrix.node-version }})'
|
||||
path: 'packages/*/junit.xml'
|
||||
reporter: 'java-junit'
|
||||
fail-on-error: 'false'
|
||||
|
||||
- name: 'Upload Test Results Artifact (for forks)'
|
||||
if: |-
|
||||
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'test-results-fork-${{ matrix.node-version }}-${{ runner.os }}'
|
||||
path: 'packages/*/junit.xml'
|
||||
|
||||
- name: 'Upload coverage reports'
|
||||
@@ -311,47 +198,9 @@ jobs:
|
||||
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'
|
||||
path: 'packages/*/coverage'
|
||||
|
||||
post_coverage_comment:
|
||||
name: 'Post Coverage Comment'
|
||||
runs-on: 'ubuntu-latest'
|
||||
needs: 'test'
|
||||
if: |-
|
||||
${{ always() && github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: 'read' # For checkout
|
||||
pull-requests: 'write' # For commenting
|
||||
strategy:
|
||||
matrix:
|
||||
# Reduce noise by only posting the comment once
|
||||
os:
|
||||
- 'ubuntu-latest'
|
||||
node-version:
|
||||
- '22.x'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
- name: 'Download coverage reports artifact'
|
||||
uses: 'actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0' # ratchet:actions/download-artifact@v5
|
||||
with:
|
||||
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'
|
||||
path: 'coverage_artifact' # Download to a specific directory
|
||||
|
||||
- name: 'Post Coverage Comment using Composite Action'
|
||||
uses: './.github/actions/post-coverage-comment' # Path to the composite action directory
|
||||
with:
|
||||
cli_json_file: 'coverage_artifact/cli/coverage/coverage-summary.json'
|
||||
core_json_file: 'coverage_artifact/core/coverage/coverage-summary.json'
|
||||
cli_full_text_summary_file: 'coverage_artifact/cli/coverage/full-text-summary.txt'
|
||||
core_full_text_summary_file: 'coverage_artifact/core/coverage/full-text-summary.txt'
|
||||
node_version: '${{ matrix.node-version }}'
|
||||
os: '${{ matrix.os }}'
|
||||
github_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
codeql:
|
||||
name: 'CodeQL'
|
||||
runs-on: 'ubuntu-latest'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions:
|
||||
actions: 'read'
|
||||
contents: 'read'
|
||||
@@ -359,6 +208,8 @@ jobs:
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
|
||||
- name: 'Initialize CodeQL'
|
||||
uses: 'github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/init@v3
|
||||
@@ -367,3 +218,107 @@ jobs:
|
||||
|
||||
- name: 'Perform CodeQL Analysis'
|
||||
uses: 'github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/analyze@v3
|
||||
|
||||
# Check for changes in bundle size.
|
||||
bundle_size:
|
||||
name: 'Check Bundle Size'
|
||||
if: "github.event_name == 'pull_request'"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions:
|
||||
contents: 'read' # For checkout
|
||||
pull-requests: 'write' # For commenting
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: 'preactjs/compressed-size-action@946a292cd35bd1088e0d7eb92b69d1a8d5b5d76a'
|
||||
with:
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
pattern: './bundle/**/*.{js,sb}'
|
||||
minimum-change-threshold: '1000'
|
||||
compression: 'none'
|
||||
clean-script: 'clean'
|
||||
|
||||
test_windows:
|
||||
name: 'Slow Test - Win'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Configure Windows Defender exclusions'
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Configure npm for Windows performance'
|
||||
run: |
|
||||
npm config set progress false
|
||||
npm config set audit false
|
||||
npm config set fund false
|
||||
npm config set loglevel error
|
||||
npm config set maxsockets 32
|
||||
npm config set registry https://registry.npmjs.org/
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'production'
|
||||
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
NO_COLOR: true
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'test'
|
||||
run: 'npm run test:ci -- --coverage.enabled=false'
|
||||
shell: 'pwsh'
|
||||
|
||||
ci:
|
||||
name: 'CI'
|
||||
if: 'always()'
|
||||
needs:
|
||||
- 'lint'
|
||||
- 'test_linux'
|
||||
- 'test_mac'
|
||||
- 'codeql'
|
||||
- 'bundle_size'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Check all job results'
|
||||
run: |
|
||||
if [[ (${{ needs.lint.result }} != 'success' && ${{ needs.lint.result }} != 'skipped') || \
|
||||
(${{ needs.test_linux.result }} != 'success' && ${{ needs.test_linux.result }} != 'skipped') || \
|
||||
(${{ needs.test_mac.result }} != 'success' && ${{ needs.test_mac.result }} != 'skipped') || \
|
||||
(${{ needs.codeql.result }} != 'success' && ${{ needs.codeql.result }} != 'skipped') || \
|
||||
(${{ needs.bundle_size.result }} != 'success' && ${{ needs.bundle_size.result }} != 'skipped') ]]; then
|
||||
echo "One or more CI jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All CI jobs passed!"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
name: 'Trigger Docs Rebuild'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'docs/**'
|
||||
jobs:
|
||||
trigger-rebuild:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Trigger rebuild'
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}' \
|
||||
"${{ secrets.DOCS_REBUILD_URL }}"
|
||||
+182
-42
@@ -1,88 +1,228 @@
|
||||
name: 'E2E Tests'
|
||||
name: 'Testing: E2E'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
# This will run for PRs from the base repository, providing secrets.
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
# This will run for PRs from forks when a label is added.
|
||||
pull_request_target:
|
||||
types: ['labeled']
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_ref:
|
||||
description: 'Branch to run on'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: 'string'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: |-
|
||||
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
|
||||
jobs:
|
||||
e2e-test-linux:
|
||||
e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'merge_group' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||
(github.event.label.name == 'maintainer:e2e:ok')
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sandbox:
|
||||
- 'sandbox:none'
|
||||
- 'sandbox:docker'
|
||||
node-version:
|
||||
- '20.x'
|
||||
- '22.x'
|
||||
- '24.x'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
- name: 'Checkout (fork)'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
if: "github.event_name == 'pull_request_target'"
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.event.pull_request.head.repo.full_name }}'
|
||||
|
||||
- name: 'Checkout (internal)'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
if: "github.event_name != 'pull_request_target'"
|
||||
with:
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: |-
|
||||
npm run build
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Set up Docker'
|
||||
if: |-
|
||||
${{ matrix.sandbox == 'sandbox:docker' }}
|
||||
if: "matrix.sandbox == 'sandbox:docker'"
|
||||
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
|
||||
|
||||
- name: 'Set up Podman'
|
||||
if: |-
|
||||
${{ matrix.sandbox == 'sandbox:podman' }}
|
||||
uses: 'redhat-actions/podman-login@4934294ad0449894bcd1e9f191899d7292469603' # ratchet:redhat-actions/podman-login@v1
|
||||
with:
|
||||
registry: 'docker.io'
|
||||
username: '${{ secrets.DOCKERHUB_USERNAME }}'
|
||||
password: '${{ secrets.DOCKERHUB_TOKEN }}'
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: '${{ matrix.sandbox }}'
|
||||
VERBOSE: 'true'
|
||||
run: |-
|
||||
npm run "test:integration:${SANDBOX}"
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
|
||||
npm run test:integration:sandbox:docker
|
||||
else
|
||||
npm run test:integration:sandbox:none
|
||||
fi
|
||||
|
||||
e2e-test-macos:
|
||||
name: 'E2E Test - macOS'
|
||||
e2e_mac:
|
||||
name: 'E2E Test (macOS)'
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'merge_group' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||
(github.event.label.name == 'maintainer:e2e:ok')
|
||||
runs-on: 'macos-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
- name: 'Checkout (fork)'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
if: "github.event_name == 'pull_request_target'"
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.event.pull_request.head.repo.full_name }}'
|
||||
|
||||
- name: 'Checkout (internal)'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
if: "github.event_name != 'pull_request_target'"
|
||||
with:
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: |-
|
||||
npm run build
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Fix rollup optional dependencies on macOS'
|
||||
if: "runner.os == 'macOS'"
|
||||
run: |
|
||||
npm cache clean --force
|
||||
- name: 'Run E2E tests (non-Windows)'
|
||||
if: "runner.os != 'Windows'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
run: 'npm run test:integration:sandbox:none'
|
||||
|
||||
e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'merge_group' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||
(github.event.label.name == 'maintainer:e2e:ok')
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- name: 'Checkout (fork)'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
if: "github.event_name == 'pull_request_target'"
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.event.pull_request.head.repo.full_name }}'
|
||||
|
||||
- name: 'Checkout (internal)'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
if: "github.event_name != 'pull_request_target'"
|
||||
with:
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Configure Windows Defender exclusions'
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Configure npm for Windows performance'
|
||||
run: |
|
||||
npm config set progress false
|
||||
npm config set audit false
|
||||
npm config set fund false
|
||||
npm config set loglevel error
|
||||
npm config set maxsockets 32
|
||||
npm config set registry https://registry.npmjs.org/
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
run: |-
|
||||
npm run test:e2e
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'test'
|
||||
shell: 'pwsh'
|
||||
run: 'npm run test:integration:sandbox:none'
|
||||
|
||||
e2e:
|
||||
name: 'E2E'
|
||||
if: |
|
||||
always() && (
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'merge_group' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||
(github.event.label.name == 'maintainer:e2e:ok')
|
||||
)
|
||||
needs:
|
||||
- 'e2e_linux'
|
||||
- 'e2e_mac'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Check E2E test results'
|
||||
run: |
|
||||
if [[ (${{ needs.e2e_linux.result }} != 'success' && ${{ needs.e2e_linux.result }} != 'skipped') || \
|
||||
(${{ needs.e2e_mac.result }} != 'success' && ${{ needs.e2e_mac.result }} != 'skipped') ]]; then
|
||||
echo "One or more E2E jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All required E2E jobs passed!"
|
||||
|
||||
+36
-17
@@ -3,27 +3,46 @@ name: 'Eval'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
packages: 'read'
|
||||
|
||||
jobs:
|
||||
eval:
|
||||
name: 'Eval'
|
||||
if: >-
|
||||
github.repository == 'google-gemini/gemini-cli'
|
||||
runs-on: 'ubuntu-latest'
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- '20.x'
|
||||
- '22.x'
|
||||
- '24.x'
|
||||
container:
|
||||
image: 'ghcr.io/google-gemini/gemini-cli-swe-agent-eval@sha256:cd5edc4afd2245c1f575e791c0859b3c084a86bb3bd9a6762296da5162b35a8f'
|
||||
credentials:
|
||||
username: '${{ github.actor }}'
|
||||
password: '${{ secrets.GITHUB_TOKEN }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
DEFAULT_VERTEXAI_PROJECT: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
GOOGLE_CLOUD_PROJECT: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
GEMINI_API_KEY: '${{ secrets.EVAL_GEMINI_API_KEY }}'
|
||||
GCLI_LOCAL_FILE_TELEMETRY: 'True'
|
||||
EVAL_GCS_BUCKET: '${{ vars.EVAL_GCS_ARTIFACTS_BUCKET }}'
|
||||
steps:
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
- name: 'Authenticate to Google Cloud'
|
||||
id: 'auth'
|
||||
uses: 'google-github-actions/auth@v2' # ratchet:exclude
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
cache: 'npm'
|
||||
project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
token_format: 'access_token'
|
||||
access_token_scopes: 'https://www.googleapis.com/auth/cloud-platform'
|
||||
|
||||
- name: 'Set up Python'
|
||||
uses: 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065' # ratchet:actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: 'Install and configure Poetry'
|
||||
uses: 'snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a' # ratchet:snok/install-poetry@v1
|
||||
- name: 'Run evaluation'
|
||||
working-directory: '/app'
|
||||
run: |
|
||||
poetry run exp_run --experiment-mode=on-demand --branch-or-commit=${{ github.ref_name }} --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15
|
||||
poetry run python agent_prototypes/scripts/parse_gcli_logs_experiment.py --experiment_dir=experiments/adhoc/gcli_temp_exp --gcs-bucket="${EVAL_GCS_BUCKET}" --gcs-path=gh_action_artifacts
|
||||
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
type: 'number'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.event.issue.number }}'
|
||||
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
@@ -29,22 +29,51 @@ permissions:
|
||||
issues: 'write'
|
||||
statuses: 'write'
|
||||
packages: 'read'
|
||||
actions: 'write' # Required for cancelling a workflow run
|
||||
|
||||
jobs:
|
||||
triage-issue:
|
||||
if: |-
|
||||
github.repository == 'google-gemini/gemini-cli' &&
|
||||
(github.event_name == 'issues' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
contains(github.event.comment.body, '@gemini-cli /triage') &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR')))
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
(github.event_name == 'issues' || github.event_name == 'issue_comment') &&
|
||||
contains(github.event.issue.labels.*.name, 'status/need-triage') &&
|
||||
(github.event_name != 'issue_comment' || (
|
||||
contains(github.event.comment.body, '@gemini-cli /triage') &&
|
||||
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')
|
||||
))
|
||||
)
|
||||
)
|
||||
timeout-minutes: 5
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- name: 'Get issue data for manual trigger'
|
||||
id: 'get_issue_data'
|
||||
if: |-
|
||||
github.event_name == 'workflow_dispatch'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
script: |
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: ${{ github.event.inputs.issue_number }},
|
||||
});
|
||||
core.setOutput('title', issue.title);
|
||||
core.setOutput('body', issue.body);
|
||||
core.setOutput('labels', issue.labels.map(label => label.name).join(','));
|
||||
return issue;
|
||||
|
||||
- name: 'Check for triage label on manual trigger'
|
||||
if: |-
|
||||
github.event_name == 'workflow_dispatch' && !contains(steps.get_issue_data.outputs.labels, 'status/need-triage')
|
||||
run: |
|
||||
echo "Issue #${{ github.event.inputs.issue_number }} does not have the 'status/need-triage' label. Stopping workflow."
|
||||
exit 1
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
|
||||
@@ -60,7 +89,7 @@ jobs:
|
||||
id: 'get_labels'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
const { data: labels } = await github.rest.issues.listLabelsForRepo({
|
||||
owner: context.repo.owner,
|
||||
@@ -76,9 +105,12 @@ jobs:
|
||||
id: 'gemini_issue_analysis'
|
||||
env:
|
||||
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
|
||||
ISSUE_TITLE: '${{ github.event.issue.title }}'
|
||||
ISSUE_BODY: '${{ github.event.issue.body }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number }}'
|
||||
ISSUE_TITLE: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.title || github.event.issue.title }}
|
||||
ISSUE_BODY: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.body || github.event.issue.body }}
|
||||
ISSUE_NUMBER: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && github.event.inputs.issue_number || github.event.issue.number }}
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
|
||||
with:
|
||||
@@ -104,18 +136,20 @@ jobs:
|
||||
## Role
|
||||
|
||||
You are an issue triage assistant. Analyze the current GitHub issue
|
||||
and identify the most appropriate existing labels. Use the available
|
||||
and identify the most appropriate existing labels by only using the provided data. Use the available
|
||||
tools to gather information; do not ask for information to be
|
||||
provided. Do not remove labels titled help wanted or good first issue.
|
||||
provided. Do not remove the following labels titled maintainer, help wanted or good first issue.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
|
||||
1. You are only able to use the echo command. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
|
||||
2. Review the issue title and body provided in the environment variables: "${ISSUE_TITLE}" and "${ISSUE_BODY}".
|
||||
3. Ignore any existing priorities or tags on the issue. Just report your findings.
|
||||
4. Select the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*. For area/* and kind/* limit yourself to only the single most applicable label in each case.
|
||||
3. Select the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*. For area/* and kind/* limit yourself to only the single most applicable label in each case.
|
||||
4. If the issue already has area/ label, dont try to change it. Similarly, if the issue already has a kind/ label don't change it. And if the issue already has a priority/ label do not change it for example:
|
||||
If an issue has area/core and kind/bug you will only add a priority/ label.
|
||||
Instead if an issue has no labels, you will could add one lable of each kind.
|
||||
5. For each issue please check if CLI version is present, this is usually in the output of the /about command and will look like 0.1.5 for anything more than 6 versions older than the most recent should add the status/need-retesting label.
|
||||
6. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label.
|
||||
6. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label and leave a comment politely requesting the relevant information, eg.. if repro steps are missing request for repro steps. if version information is missing request for version information into the explanation section below.
|
||||
7. Output the appropriate labels for this issue in JSON format with explanation, for example:
|
||||
```
|
||||
{"labels_to_set": ["kind/bug", "priority/p0"], "explanation": "This is a critical bug report affecting main functionality"}
|
||||
@@ -125,6 +159,8 @@ jobs:
|
||||
{"labels_to_set": [], "explanation": "Unable to classify this issue with available labels"}
|
||||
```
|
||||
9. Use Area definitions mentioned below to help you narrow down issues.
|
||||
10. If you think an issue might be a Priority/P0 do not apply the priority/p0 label. Instead apply a status/manual-triage label and include a note in your explanation.
|
||||
11. If you are uncertain and have not been able to apply one each of kind/, area/ and priority/ , apply the status/manual-triage label.
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -141,7 +177,10 @@ jobs:
|
||||
|
||||
Categorization Guidelines:
|
||||
P0: Critical / Blocker
|
||||
- A P0 bug is a catastrophic failure that demands immediate attention. It represents a complete showstopper for a significant portion of users or for the development process itself.
|
||||
- A P0 bug is a catastrophic failure that demands immediate attention.
|
||||
- To be a P0 it means almost all users are running into this issue and it is blocking users from being able to use the product.
|
||||
- You would see this in the form of many comments from different developers on the bug.
|
||||
- It represents a complete showstopper for a significant portion of users or for the development process itself.
|
||||
Impact:
|
||||
- Blocks development or testing for the entire team.
|
||||
- Major security vulnerability that could compromise user data or system integrity.
|
||||
@@ -150,14 +189,17 @@ jobs:
|
||||
Qualifier: Is the main function of the software broken?
|
||||
Example: The gemini auth login command fails with an unrecoverable error, preventing any user from authenticating and using the rest of the CLI.
|
||||
P1: High
|
||||
- A P1 bug is a serious issue that significantly degrades the user experience or impacts a core feature. While not a complete blocker, it's a major problem that needs a fast resolution. Feature requests are almost never P1.
|
||||
- A P1 bug is a serious issue that significantly degrades the user experience or impacts a core feature.
|
||||
- While not a complete blocker, it's a major problem that needs a fast resolution. Feature requests are almost never P1.
|
||||
- Once again this would be affecting many users.
|
||||
- You would see this in the form of comments from different developers on the bug.
|
||||
Impact:
|
||||
- A core feature is broken or behaving incorrectly for a large number of users or large number of use cases.
|
||||
- Review the bug details and comments to try figure out if this issue affects a large set of use cases or if it's a narrow set of use cases.
|
||||
- Severe performance degradation making the application frustratingly slow.
|
||||
- No straightforward workaround exists, or the workaround is difficult and non-obvious.
|
||||
Qualifier: Is a key feature unusable or giving very wrong results?
|
||||
Example: The gemini -p "..." command consistently returns a malformed JSON response or an empty result, making the CLI's primary generation feature unreliable.
|
||||
Example: Gemini CLI enters a loop when making read-many-files tool call. I am unable to break out of the loop and gemini doesn't follow instructions subsequently.
|
||||
P2: Medium
|
||||
- A P2 bug is a moderately impactful issue. It's a noticeable problem but doesn't prevent the use of the software's main functionality.
|
||||
Impact:
|
||||
@@ -221,19 +263,23 @@ jobs:
|
||||
${{ steps.gemini_issue_analysis.outputs.summary != '' }}
|
||||
env:
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
// Strip code block markers if present
|
||||
const rawLabels = process.env.LABELS_OUTPUT;
|
||||
core.info(`Raw labels JSON: ${rawLabels}`);
|
||||
let parsedLabels;
|
||||
try {
|
||||
const trimmedLabels = rawLabels.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim();
|
||||
parsedLabels = JSON.parse(trimmedLabels);
|
||||
const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (!jsonMatch || !jsonMatch[1]) {
|
||||
throw new Error("Could not find a ```json ... ``` block in the output.");
|
||||
}
|
||||
const jsonString = jsonMatch[1].trim();
|
||||
parsedLabels = JSON.parse(jsonString);
|
||||
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
|
||||
} catch (err) {
|
||||
core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
|
||||
@@ -241,32 +287,44 @@ jobs:
|
||||
}
|
||||
|
||||
const issueNumber = parseInt(process.env.ISSUE_NUMBER);
|
||||
const explanation = parsedLabels.explanation || '';
|
||||
const labelsToSet = parsedLabels.labels_to_set || [];
|
||||
labelsToSet.push('status/bot-triaged');
|
||||
|
||||
// Set labels based on triage result
|
||||
if (parsedLabels.labels_to_set && parsedLabels.labels_to_set.length > 0) {
|
||||
if (labelsToSet.length > 0) {
|
||||
await github.rest.issues.setLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: parsedLabels.labels_to_set
|
||||
labels: labelsToSet
|
||||
});
|
||||
const explanation = parsedLabels.explanation ? ` - ${parsedLabels.explanation}` : '';
|
||||
core.info(`Successfully set labels for #${issueNumber}: ${parsedLabels.labels_to_set.join(', ')}${explanation}`);
|
||||
const explanationInfo = explanation ? ` - ${explanation}` : '';
|
||||
core.info(`Successfully set labels for #${issueNumber}: ${labelsToSet.join(', ')}${explanationInfo}`);
|
||||
} else {
|
||||
// If no labels to set, leave the issue as is
|
||||
const explanation = parsedLabels.explanation ? ` - ${parsedLabels.explanation}` : '';
|
||||
core.info(`No labels to set for #${issueNumber}, leaving as is${explanation}`);
|
||||
const explanationInfo = explanation ? ` - ${explanation}` : '';
|
||||
core.info(`No labels to set for #${issueNumber}, leaving as is${explanationInfo}`);
|
||||
}
|
||||
|
||||
if (explanation) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: explanation,
|
||||
});
|
||||
}
|
||||
|
||||
- name: 'Post Issue Analysis Failure Comment'
|
||||
if: |-
|
||||
${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }}
|
||||
env:
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
echo '🏷️ Finding issues that need triage...'
|
||||
NEED_TRIAGE_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
--search 'is:open is:issue label:"status/needs-triage"' --json number,title,body)"
|
||||
--search "is:open is:issue label:\"status/need-triage\"" --limit 1000 --json number,title,body)"
|
||||
|
||||
echo '🔄 Merging and deduplicating issues...'
|
||||
ISSUES="$(echo "${NO_LABEL_ISSUES}" "${NEED_TRIAGE_ISSUES}" | jq -c -s 'add | unique_by(.number)')"
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
id: 'get_labels'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
const { data: labels } = await github.rest.issues.listLabelsForRepo({
|
||||
owner: context.repo.owner,
|
||||
@@ -113,11 +113,13 @@ jobs:
|
||||
|
||||
## Steps
|
||||
|
||||
1. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
|
||||
1. You are only able to use the echo command. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
|
||||
2. Check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues)
|
||||
3. Review the issue title, body and any comments provided in the environment variables.
|
||||
4. Ignore any existing priorities or tags on the issue.
|
||||
5. Identify the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*.
|
||||
4. Identify the most relevant labels from the existing labels, focusing on kind/*, area/*, sub-area/* and priority/*.
|
||||
5. If the issue already has area/ label, dont try to change it. Similarly, if the issue already has a kind/ label don't change it. And if the issue already has a priority/ label do not change it for example:
|
||||
If an issue has area/core and kind/bug you will only add a priority/ label.
|
||||
Instead if an issue has no labels, you will could add one lable of each kind.
|
||||
6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc.
|
||||
7. For area/* and kind/* limit yourself to only the single most applicable label in each case.
|
||||
8. Give me a single short explanation about why you are selecting each label in the process.
|
||||
@@ -128,7 +130,7 @@ jobs:
|
||||
{
|
||||
"issue_number": 123,
|
||||
"labels_to_add": ["kind/bug", "priority/p2"],
|
||||
"labels_to_remove": ["status/needs-triage"],
|
||||
"labels_to_remove": ["status/need-triage"],
|
||||
"explanation": "This issue is a bug that needs to be addressed with medium priority."
|
||||
},
|
||||
{
|
||||
@@ -142,8 +144,10 @@ jobs:
|
||||
If an issue cannot be classified, do not include it in the output array.
|
||||
10. For each issue please check if CLI version is present, this is usually in the output of the /about command and will look like 0.1.5
|
||||
- Anything more than 6 versions older than the most recent should add the status/need-retesting label
|
||||
11. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label
|
||||
11. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label and leave a comment politely requesting the relevant information, eg.. if repro steps are missing request for repro steps. if version information is missing request for version information into the explanation section below.
|
||||
- After identifying appropriate labels to an issue, add "status/need-triage" label to labels_to_remove in the output.
|
||||
12. If you think an issue might be a Priority/P0 do not apply the priority/p0 label. Instead apply a status/manual-triage label and include a note in your explanation.
|
||||
13. If you are uncertain and have not been able to apply one each of kind/, area/ and priority/ , apply the status/manual-triage label.
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -151,7 +155,7 @@ jobs:
|
||||
- Do not include any explanation or additional text, just the JSON
|
||||
- Only use labels that already exist in the repository.
|
||||
- Do not add comments or modify the issue content.
|
||||
- Do not remove labels titled help wanted or good first issue.
|
||||
- Do not remove the following labels maintainer, help wanted or good first issue.
|
||||
- Triage only the current issue.
|
||||
- Identify only one area/ label
|
||||
- Identify only one kind/ label (Do not apply kind/duplicate or kind/parent-issue)
|
||||
@@ -159,8 +163,11 @@ jobs:
|
||||
- Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario.
|
||||
Categorization Guidelines:
|
||||
P0: Critical / Blocker
|
||||
- A P0 bug is a catastrophic failure that demands immediate attention. It represents a complete showstopper for a significant portion of users or for the development process itself.
|
||||
Impact:
|
||||
- A P0 bug is a catastrophic failure that demands immediate attention.
|
||||
- To be a P0 it means almost all users are running into this issue and it is blocking users from being able to use the product.
|
||||
- You would see this in the form of many comments from different developers on the bug.
|
||||
- It represents a complete showstopper for a significant portion of users or for the development process itself.
|
||||
Impact:
|
||||
- Blocks development or testing for the entire team.
|
||||
- Major security vulnerability that could compromise user data or system integrity.
|
||||
- Causes data loss or corruption with no workaround.
|
||||
@@ -169,15 +176,17 @@ jobs:
|
||||
Qualifier: Is the main function of the software broken?
|
||||
Example: The gemini auth login command fails with an unrecoverable error, preventing any user from authenticating and using the rest of the CLI.
|
||||
P1: High
|
||||
- A P1 bug is a serious issue that significantly degrades the user experience or impacts a core feature. While not a complete blocker, it's a major problem that needs a fast resolution.
|
||||
- Feature requests are almost never P1.
|
||||
- A P1 bug is a serious issue that significantly degrades the user experience or impacts a core feature.
|
||||
- While not a complete blocker, it's a major problem that needs a fast resolution. Feature requests are almost never P1.
|
||||
- Once again this would be affecting many users.
|
||||
- You would see this in the form of comments from different developers on the bug.
|
||||
Impact:
|
||||
- A core feature is broken or behaving incorrectly for a large number of users or large number of use cases.
|
||||
- Review the bug details and comments to try figure out if this issue affects a large set of use cases or if it's a narrow set of use cases.
|
||||
- Severe performance degradation making the application frustratingly slow.
|
||||
- No straightforward workaround exists, or the workaround is difficult and non-obvious.
|
||||
Qualifier: Is a key feature unusable or giving very wrong results?
|
||||
Example: The gemini -p "..." command consistently returns a malformed JSON response or an empty result, making the CLI's primary generation feature unreliable.
|
||||
Example: Gemini CLI enters a loop when making read-many-files tool call. I am unable to break out of the loop and gemini doesn't follow instructions subsequently.
|
||||
P2: Medium
|
||||
- A P2 bug is a moderately impactful issue. It's a noticeable problem but doesn't prevent the use of the software's main functionality.
|
||||
Impact:
|
||||
@@ -252,15 +261,18 @@ jobs:
|
||||
LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
// Strip code block markers if present
|
||||
const rawLabels = process.env.LABELS_OUTPUT;
|
||||
core.info(`Raw labels JSON: ${rawLabels}`);
|
||||
let parsedLabels;
|
||||
try {
|
||||
const trimmedLabels = rawLabels.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim();
|
||||
parsedLabels = JSON.parse(trimmedLabels);
|
||||
const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (!jsonMatch || !jsonMatch[1]) {
|
||||
throw new Error("Could not find a ```json ... ``` block in the output.");
|
||||
}
|
||||
const jsonString = jsonMatch[1].trim();
|
||||
parsedLabels = JSON.parse(jsonString);
|
||||
core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
|
||||
} catch (err) {
|
||||
core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
|
||||
@@ -274,18 +286,48 @@ jobs:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set labels based on triage result
|
||||
if (entry.labels_to_set && entry.labels_to_set.length > 0) {
|
||||
await github.rest.issues.setLabels({
|
||||
const labelsToAdd = entry.labels_to_add || [];
|
||||
labelsToAdd.push('status/bot-triaged');
|
||||
|
||||
if (labelsToAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: entry.labels_to_set
|
||||
labels: labelsToAdd
|
||||
});
|
||||
const explanation = entry.explanation ? ` - ${entry.explanation}` : '';
|
||||
core.info(`Successfully set labels for #${issueNumber}: ${entry.labels_to_set.join(', ')}${explanation}`);
|
||||
} else {
|
||||
// If no labels to set, leave the issue as is
|
||||
core.info(`No labels to set for #${issueNumber}, leaving as is`);
|
||||
core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}${explanation}`);
|
||||
}
|
||||
|
||||
if (entry.labels_to_remove && entry.labels_to_remove.length > 0) {
|
||||
for (const label of entry.labels_to_remove) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
name: label
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
core.info(`Successfully removed labels for #${issueNumber}: ${entry.labels_to_remove.join(', ')}`);
|
||||
}
|
||||
|
||||
if (entry.explanation) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: entry.explanation,
|
||||
});
|
||||
}
|
||||
|
||||
if ((!entry.labels_to_add || entry.labels_to_add.length === 0) && (!entry.labels_to_remove || entry.labels_to_remove.length === 0)) {
|
||||
core.info(`No labels to add or remove for #${issueNumber}, leaving as is`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ jobs:
|
||||
|
||||
// Search for open issues already assigned to the commenter in this repo
|
||||
const { data: assignedIssues } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `is:issue repo:${owner}/${repo} assignee:${commenter} is:open`
|
||||
q: `is:issue repo:${owner}/${repo} assignee:${commenter} is:open`,
|
||||
advanced_search: true
|
||||
});
|
||||
|
||||
if (assignedIssues.total_count >= MAX_ISSUES_ASSIGNED) {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
name: 'Release: Change Tags'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'The package version to tag (e.g., 0.5.0-preview-2). This version must already exist on the npm registry.'
|
||||
required: true
|
||||
type: 'string'
|
||||
channel:
|
||||
description: 'The npm dist-tag to apply (e.g., latest, preview, nightly).'
|
||||
required: true
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'latest'
|
||||
- 'preview'
|
||||
- 'nightly'
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to run from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: 'https://wombat-dressing-room.appspot.com'
|
||||
scope: '@google'
|
||||
|
||||
- name: 'Change tag'
|
||||
uses: './.github/actions/tag-npm-release'
|
||||
with:
|
||||
channel: '${{ github.event.inputs.channel }}'
|
||||
version: '${{ github.event.inputs.version }}'
|
||||
dry-run: '${{ github.event.inputs.dry-run }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
@@ -0,0 +1,113 @@
|
||||
name: 'Release: Manual'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'The version to release (e.g., v0.1.11). Must be a valid semver string with a "v" prefix.'
|
||||
required: true
|
||||
type: 'string'
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: true
|
||||
type: 'string'
|
||||
npm_channel:
|
||||
description: 'The npm channel to publish to.'
|
||||
required: true
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'preview'
|
||||
- 'nightly'
|
||||
- 'latest'
|
||||
- 'dev'
|
||||
default: 'dev'
|
||||
dry_run:
|
||||
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
skip_github_release:
|
||||
description: 'Select to skip creating a GitHub release and create a npm release only.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: 'self-hosted'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Checkout Release Code'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Debug Inputs'
|
||||
run: 'echo "${{ toJSON(inputs) }}"'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Prepare Release Info'
|
||||
id: 'release_info'
|
||||
working-directory: './release'
|
||||
run: |
|
||||
RELEASE_VERSION="${{ github.event.inputs.version }}"
|
||||
echo "RELEASE_VERSION=${RELEASE_VERSION#v}" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_TAG=$(git describe --tags --abbrev=0)" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{github.event.inputs.force_skip_tests != 'true'}}"
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Publish Release'
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
force-skip-tests: '${{ github.event.inputs.force_skip_tests }}'
|
||||
release-version: '${{ steps.release_info.outputs.RELEASE_VERSION }}'
|
||||
release-tag: '${{ github.event.inputs.version }}'
|
||||
npm-tag: '${{ github.event.inputs.npm_channel }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ steps.release_info.outputs.PREVIOUS_TAG }}'
|
||||
skip-github-release: '${{ github.event.inputs.skip_github_release }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: '${{ github.event.inputs.version }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Manual Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The manual release workflow failed. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'kind/bug,release-failure,priority/p0'
|
||||
@@ -0,0 +1,126 @@
|
||||
name: 'Release: Nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: true
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Checkout Release Code'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './release/.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Print Inputs'
|
||||
run: |
|
||||
echo "${{ toJSON(github.event.inputs) }}"
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{github.event.inputs.force_skip_tests == 'false'}}"
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Get Nightly Version'
|
||||
id: 'nightly_version'
|
||||
working-directory: './release'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# Calculate the version using the centralized script
|
||||
VERSION_JSON=$(node scripts/get-release-version.js --type=nightly)
|
||||
|
||||
# Extract values for logging and outputs
|
||||
RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)
|
||||
RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)
|
||||
NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)
|
||||
PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)
|
||||
|
||||
# Print calculated values for logging
|
||||
echo "Calculated Release Tag: ${RELEASE_TAG}"
|
||||
echo "Calculated Release Version: ${RELEASE_VERSION}"
|
||||
echo "Calculated Previous Tag: ${PREVIOUS_TAG}"
|
||||
|
||||
# Set outputs for subsequent steps
|
||||
echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
echo "NPM_TAG=${NPM_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Publish Release'
|
||||
if: true
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
|
||||
release-tag: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
npm-tag: '${{ steps.nightly_version.outputs.NPM_TAG }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create and Merge Pull Request'
|
||||
uses: './.github/actions/create-pull-request'
|
||||
with:
|
||||
branch-name: 'release/${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
pr-title: 'chore(release): bump version to ${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
|
||||
pr-body: 'Automated version bump for nightly release.'
|
||||
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title "Nightly Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
|
||||
--body "The nightly-release workflow failed. See the full run for details: ${DETAILS_URL}" \
|
||||
--label 'kind/bug,release-failure,priority/p0'
|
||||
@@ -0,0 +1,187 @@
|
||||
name: 'Release: Patch (0) from Comment'
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: ['created']
|
||||
|
||||
jobs:
|
||||
slash-command:
|
||||
runs-on: 'ubuntu-latest'
|
||||
# Only run if the comment is from a human user (not automated)
|
||||
if: "github.event.comment.user.type == 'User' && github.event.comment.user.login != 'github-actions[bot]'"
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
actions: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 'Slash Command Dispatch'
|
||||
id: 'slash_command'
|
||||
uses: 'peter-evans/slash-command-dispatch@40877f718dce0101edfc7aea2b3800cc192f9ed5'
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
commands: 'patch'
|
||||
permission: 'write'
|
||||
issue-type: 'pull-request'
|
||||
|
||||
- name: 'Get PR Status'
|
||||
id: 'pr_status'
|
||||
if: "startsWith(github.event.comment.body, '/patch')"
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
gh pr view "${{ github.event.issue.number }}" --json mergeCommit,state > pr_status.json
|
||||
echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> "$GITHUB_OUTPUT"
|
||||
echo "STATE=$(jq -r .state pr_status.json)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Dispatch if Merged'
|
||||
if: "steps.pr_status.outputs.STATE == 'MERGED'"
|
||||
id: 'dispatch_patch'
|
||||
uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325'
|
||||
env:
|
||||
COMMENT_BODY: '${{ github.event.comment.body }}'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
script: |
|
||||
// Parse the comment body directly to extract channel(s)
|
||||
const commentBody = process.env.COMMENT_BODY;
|
||||
console.log('Comment body:', commentBody);
|
||||
|
||||
let channels = ['stable', 'preview']; // default to both
|
||||
|
||||
// Parse different formats:
|
||||
// /patch (defaults to both)
|
||||
// /patch both
|
||||
// /patch stable
|
||||
// /patch preview
|
||||
if (commentBody.trim() === '/patch' || commentBody.trim() === '/patch both') {
|
||||
channels = ['stable', 'preview'];
|
||||
} else if (commentBody.trim() === '/patch stable') {
|
||||
channels = ['stable'];
|
||||
} else if (commentBody.trim() === '/patch preview') {
|
||||
channels = ['preview'];
|
||||
} else {
|
||||
// Fallback parsing for legacy formats
|
||||
if (commentBody.includes('channel=preview')) {
|
||||
channels = ['preview'];
|
||||
} else if (commentBody.includes('--channel preview')) {
|
||||
channels = ['preview'];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Detected channels:', channels);
|
||||
|
||||
const dispatchedRuns = [];
|
||||
|
||||
// Dispatch workflow for each channel
|
||||
for (const channel of channels) {
|
||||
console.log(`Dispatching workflow for channel: ${channel}`);
|
||||
|
||||
const response = await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'release-patch-1-create-pr.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}',
|
||||
channel: channel,
|
||||
original_pr: '${{ github.event.issue.number }}'
|
||||
}
|
||||
});
|
||||
|
||||
dispatchedRuns.push({ channel, response });
|
||||
}
|
||||
|
||||
// Wait a moment for the workflows to be created
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'release-patch-1-create-pr.yml',
|
||||
per_page: 20 // Increased to handle multiple runs
|
||||
});
|
||||
|
||||
// Find the recent runs that match our trigger
|
||||
const recentRuns = runs.data.workflow_runs.filter(run =>
|
||||
run.event === 'workflow_dispatch' &&
|
||||
new Date(run.created_at) > new Date(Date.now() - 15000) // Within last 15 seconds
|
||||
).slice(0, channels.length); // Limit to the number of channels we dispatched
|
||||
|
||||
// Set outputs
|
||||
core.setOutput('dispatched_channels', channels.join(','));
|
||||
core.setOutput('dispatched_run_count', channels.length.toString());
|
||||
|
||||
if (recentRuns.length > 0) {
|
||||
core.setOutput('dispatched_run_urls', recentRuns.map(r => r.html_url).join(','));
|
||||
core.setOutput('dispatched_run_ids', recentRuns.map(r => r.id).join(','));
|
||||
}
|
||||
|
||||
- name: 'Comment on Failure'
|
||||
if: "startsWith(github.event.comment.body, '/patch') && steps.pr_status.outputs.STATE != 'MERGED'"
|
||||
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
:x: The `/patch` command failed. This pull request must be merged before a patch can be created.
|
||||
|
||||
- name: 'Final Status Comment - Success'
|
||||
if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && steps.dispatch_patch.outputs.dispatched_run_urls"
|
||||
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
✅ **Patch workflow(s) dispatched successfully!**
|
||||
|
||||
**📋 Details:**
|
||||
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
|
||||
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
|
||||
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
|
||||
|
||||
**🔗 Track Progress:**
|
||||
- [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
- name: 'Final Status Comment - Dispatch Success (No URL)'
|
||||
if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && !steps.dispatch_patch.outputs.dispatched_run_urls"
|
||||
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
✅ **Patch workflow(s) dispatched successfully!**
|
||||
|
||||
**📋 Details:**
|
||||
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
|
||||
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
|
||||
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
|
||||
|
||||
**🔗 Track Progress:**
|
||||
- [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
- name: 'Final Status Comment - Failure'
|
||||
if: "always() && startsWith(github.event.comment.body, '/patch') && (steps.dispatch_patch.outcome == 'failure' || steps.dispatch_patch.outcome == 'cancelled')"
|
||||
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
❌ **Patch workflow dispatch failed!**
|
||||
|
||||
There was an error dispatching the patch creation workflow.
|
||||
|
||||
**🔍 Troubleshooting:**
|
||||
- Check that the PR is properly merged
|
||||
- Verify workflow permissions
|
||||
- Review error logs in the workflow run
|
||||
|
||||
**🔗 Debug Links:**
|
||||
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
- [Patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
@@ -0,0 +1,100 @@
|
||||
name: 'Release: Patch (1) Create PR'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit:
|
||||
description: 'The commit SHA to cherry-pick for the patch.'
|
||||
required: true
|
||||
type: 'string'
|
||||
channel:
|
||||
description: 'The release channel to patch.'
|
||||
required: true
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'stable'
|
||||
- 'preview'
|
||||
dry_run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to test from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
original_pr:
|
||||
description: 'The original PR number to comment back on.'
|
||||
required: false
|
||||
type: 'string'
|
||||
|
||||
jobs:
|
||||
create-patch:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
actions: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Script Dependencies'
|
||||
run: 'npm install yargs'
|
||||
|
||||
- name: 'Configure Git User'
|
||||
run: |-
|
||||
git config user.name "gemini-cli-robot"
|
||||
git config user.email "gemini-cli-robot@google.com"
|
||||
# Configure git to use GITHUB_TOKEN for remote operations (has actions:write for workflow files)
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git"
|
||||
|
||||
- name: 'Create Patch'
|
||||
id: 'create_patch'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Capture output and display it in logs using tee
|
||||
{
|
||||
node scripts/releasing/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }} --pullRequestNumber=${{ github.event.inputs.original_pr }} --dry-run=${{ github.event.inputs.dry_run }}
|
||||
echo "EXIT_CODE=$?" >> "$GITHUB_OUTPUT"
|
||||
} 2>&1 | tee >(
|
||||
echo "LOG_CONTENT<<EOF" >> "$GITHUB_ENV"
|
||||
cat >> "$GITHUB_ENV"
|
||||
echo "EOF" >> "$GITHUB_ENV"
|
||||
)
|
||||
|
||||
- name: 'Comment on Original PR'
|
||||
if: 'always() && inputs.original_pr'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
|
||||
EXIT_CODE: '${{ steps.create_patch.outputs.EXIT_CODE }}'
|
||||
COMMIT: '${{ github.event.inputs.commit }}'
|
||||
CHANNEL: '${{ github.event.inputs.channel }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
GITHUB_RUN_ID: '${{ github.run_id }}'
|
||||
LOG_CONTENT: '${{ env.LOG_CONTENT }}'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git checkout '${{ github.event.inputs.ref }}'
|
||||
node scripts/releasing/patch-create-comment.js
|
||||
|
||||
- name: 'Fail Workflow if Main Task Failed'
|
||||
if: 'always() && steps.create_patch.outputs.EXIT_CODE != 0'
|
||||
run: |
|
||||
echo "Patch creation failed with exit code: ${{ steps.create_patch.outputs.EXIT_CODE }}"
|
||||
echo "Check the logs above and the comment posted to the original PR for details."
|
||||
exit 1
|
||||
@@ -0,0 +1,78 @@
|
||||
name: 'Release: Patch (2) Trigger'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- 'closed'
|
||||
branches:
|
||||
- 'release/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'The head ref of the merged hotfix PR to trigger the release for (e.g. hotfix/v1.2.3/cherry-pick-abc).'
|
||||
required: true
|
||||
type: 'string'
|
||||
workflow_ref:
|
||||
description: 'The ref to checkout the workflow code from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
workflow_id:
|
||||
description: 'The workflow to trigger. Defaults to patch-release.yml'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'release-patch-3-release.yml'
|
||||
dry_run:
|
||||
description: 'Whether this is a dry run.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
test_mode:
|
||||
description: 'Whether or not to run in test mode'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
trigger-patch-release:
|
||||
if: "(github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'hotfix/')) || github.event_name == 'workflow_dispatch'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
actions: 'write'
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: "${{ github.event.inputs.workflow_ref || 'main' }}"
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Trigger Patch Release'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
HEAD_REF: "${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.event.inputs.ref }}"
|
||||
PR_BODY: "${{ github.event_name == 'pull_request' && github.event.pull_request.body || '' }}"
|
||||
WORKFLOW_ID: '${{ github.event.inputs.workflow_id }}'
|
||||
GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
|
||||
GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
|
||||
GITHUB_EVENT_NAME: '${{ github.event_name }}'
|
||||
GITHUB_EVENT_PAYLOAD: '${{ toJSON(github.event) }}'
|
||||
FORCE_SKIP_TESTS: '${{ github.event.inputs.force_skip_tests }}'
|
||||
TEST_MODE: '${{ github.event.inputs.test_mode }}'
|
||||
run: |
|
||||
node scripts/releasing/patch-trigger.js --dry-run=${{ github.event.inputs.dry_run }}
|
||||
@@ -0,0 +1,219 @@
|
||||
name: 'Release: Patch (3) Release'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
type:
|
||||
description: 'The type of release to perform.'
|
||||
required: true
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'stable'
|
||||
- 'preview'
|
||||
dry_run:
|
||||
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
release_ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: true
|
||||
type: 'string'
|
||||
original_pr:
|
||||
description: 'The original PR number to comment back on.'
|
||||
required: false
|
||||
type: 'string'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: 'Checkout Release Code'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.release_ref }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: |-
|
||||
npm ci
|
||||
|
||||
- name: 'Print Inputs'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
echo "${{ toJSON(inputs) }}"
|
||||
|
||||
- name: 'Get Patch Version'
|
||||
id: 'patch_version'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# Use the existing get-release-version.js script to calculate patch version
|
||||
# Run from main checkout which has full git history and access to npm
|
||||
PATCH_JSON=$(node scripts/get-release-version.js --type=patch --patch-from=${{ github.event.inputs.type }})
|
||||
echo "Patch version calculation result: ${PATCH_JSON}"
|
||||
|
||||
RELEASE_VERSION=$(echo "${PATCH_JSON}" | jq -r .releaseVersion)
|
||||
RELEASE_TAG=$(echo "${PATCH_JSON}" | jq -r .releaseTag)
|
||||
NPM_TAG=$(echo "${PATCH_JSON}" | jq -r .npmTag)
|
||||
PREVIOUS_TAG=$(echo "${PATCH_JSON}" | jq -r .previousReleaseTag)
|
||||
|
||||
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
echo "NPM_TAG=${NPM_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Verify Version Consistency'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
CHANNEL: '${{ github.event.inputs.type }}'
|
||||
run: |
|
||||
echo "🔍 Verifying no concurrent patch releases have occurred..."
|
||||
|
||||
# Store original calculation for comparison
|
||||
ORIGINAL_RELEASE_VERSION="${{ steps.patch_version.outputs.RELEASE_VERSION }}"
|
||||
ORIGINAL_RELEASE_TAG="${{ steps.patch_version.outputs.RELEASE_TAG }}"
|
||||
ORIGINAL_PREVIOUS_TAG="${{ steps.patch_version.outputs.PREVIOUS_TAG }}"
|
||||
|
||||
echo "Original calculation:"
|
||||
echo " Release version: ${ORIGINAL_RELEASE_VERSION}"
|
||||
echo " Release tag: ${ORIGINAL_RELEASE_TAG}"
|
||||
echo " Previous tag: ${ORIGINAL_PREVIOUS_TAG}"
|
||||
|
||||
# Re-run the same version calculation script
|
||||
echo "Re-calculating version to check for changes..."
|
||||
CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --type=patch --patch-from="${CHANNEL}")
|
||||
CURRENT_RELEASE_VERSION=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseVersion)
|
||||
CURRENT_RELEASE_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseTag)
|
||||
CURRENT_PREVIOUS_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .previousReleaseTag)
|
||||
|
||||
echo "Current calculation:"
|
||||
echo " Release version: ${CURRENT_RELEASE_VERSION}"
|
||||
echo " Release tag: ${CURRENT_RELEASE_TAG}"
|
||||
echo " Previous tag: ${CURRENT_PREVIOUS_TAG}"
|
||||
|
||||
# Compare calculations
|
||||
if [[ "${ORIGINAL_RELEASE_VERSION}" != "${CURRENT_RELEASE_VERSION}" ]] || \
|
||||
[[ "${ORIGINAL_RELEASE_TAG}" != "${CURRENT_RELEASE_TAG}" ]] || \
|
||||
[[ "${ORIGINAL_PREVIOUS_TAG}" != "${CURRENT_PREVIOUS_TAG}" ]]; then
|
||||
echo "❌ RACE CONDITION DETECTED: Version calculations have changed!"
|
||||
echo "This indicates another patch release completed while this one was in progress."
|
||||
echo ""
|
||||
echo "Originally planned: ${ORIGINAL_RELEASE_VERSION} (from ${ORIGINAL_PREVIOUS_TAG})"
|
||||
echo "Should now build: ${CURRENT_RELEASE_VERSION} (from ${CURRENT_PREVIOUS_TAG})"
|
||||
echo ""
|
||||
echo "# Setting outputs for failure comment"
|
||||
echo "CURRENT_RELEASE_VERSION=${CURRENT_RELEASE_VERSION}" >> "${GITHUB_ENV}"
|
||||
echo "CURRENT_RELEASE_TAG=${CURRENT_RELEASE_TAG}" >> "${GITHUB_ENV}"
|
||||
echo "CURRENT_PREVIOUS_TAG=${CURRENT_PREVIOUS_TAG}" >> "${GITHUB_ENV}"
|
||||
echo "The patch release must be restarted to use the correct version numbers."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Version calculations unchanged - proceeding with release"
|
||||
|
||||
- name: 'Print Calculated Version'
|
||||
run: |-
|
||||
echo "Patch Release Summary:"
|
||||
echo " Release Version: ${{ steps.patch_version.outputs.RELEASE_VERSION }}"
|
||||
echo " Release Tag: ${{ steps.patch_version.outputs.RELEASE_TAG }}"
|
||||
echo " NPM Tag: ${{ steps.patch_version.outputs.NPM_TAG }}"
|
||||
echo " Previous Tag: ${{ steps.patch_version.outputs.PREVIOUS_TAG }}"
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{github.event.inputs.force_skip_tests != 'true'}}"
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Publish Release'
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
|
||||
release-tag: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
npm-tag: '${{ steps.patch_version.outputs.NPM_TAG }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Patch Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The patch-release workflow failed. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'kind/bug,release-failure,priority/p0'
|
||||
|
||||
- name: 'Comment Success on Original PR'
|
||||
if: '${{ success() && github.event.inputs.original_pr }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
|
||||
SUCCESS: 'true'
|
||||
RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
|
||||
RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
|
||||
CHANNEL: '${{ github.event.inputs.type }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
GITHUB_RUN_ID: '${{ github.run_id }}'
|
||||
GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
|
||||
GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
|
||||
run: |
|
||||
node scripts/releasing/patch-comment.js
|
||||
|
||||
- name: 'Comment Failure on Original PR'
|
||||
if: '${{ failure() && github.event.inputs.original_pr }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
|
||||
SUCCESS: 'false'
|
||||
RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
|
||||
RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
|
||||
CHANNEL: '${{ github.event.inputs.type }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
GITHUB_RUN_ID: '${{ github.run_id }}'
|
||||
GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
|
||||
GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
|
||||
# Pass current version info for race condition failures
|
||||
CURRENT_RELEASE_VERSION: '${{ env.CURRENT_RELEASE_VERSION }}'
|
||||
CURRENT_RELEASE_TAG: '${{ env.CURRENT_RELEASE_TAG }}'
|
||||
CURRENT_PREVIOUS_TAG: '${{ env.CURRENT_PREVIOUS_TAG }}'
|
||||
run: |
|
||||
# Check if this was a version consistency failure
|
||||
if [[ -n "${CURRENT_RELEASE_VERSION}" ]]; then
|
||||
echo "Detected version race condition failure - posting specific comment with current version info"
|
||||
export RACE_CONDITION_FAILURE=true
|
||||
fi
|
||||
node scripts/releasing/patch-comment.js
|
||||
@@ -0,0 +1,336 @@
|
||||
name: 'Release: Promote'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
stable_version_override:
|
||||
description: 'Manually override the stable version number.'
|
||||
required: false
|
||||
type: 'string'
|
||||
preview_version_override:
|
||||
description: 'Manually override the preview version number.'
|
||||
required: false
|
||||
type: 'string'
|
||||
|
||||
jobs:
|
||||
calculate-versions:
|
||||
name: 'Calculate Versions and Plan'
|
||||
runs-on: 'ubuntu-latest'
|
||||
outputs:
|
||||
STABLE_VERSION: '${{ steps.versions.outputs.STABLE_VERSION }}'
|
||||
STABLE_SHA: '${{ steps.versions.outputs.STABLE_SHA }}'
|
||||
PREVIOUS_STABLE_TAG: '${{ steps.versions.outputs.PREVIOUS_STABLE_TAG }}'
|
||||
PREVIEW_VERSION: '${{ steps.versions.outputs.PREVIEW_VERSION }}'
|
||||
PREVIEW_SHA: '${{ steps.versions.outputs.PREVIEW_SHA }}'
|
||||
PREVIOUS_PREVIEW_TAG: '${{ steps.versions.outputs.PREVIOUS_PREVIEW_TAG }}'
|
||||
NEXT_NIGHTLY_VERSION: '${{ steps.versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
PREVIOUS_NIGHTLY_TAG: '${{ steps.versions.outputs.PREVIOUS_NIGHTLY_TAG }}'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Print Inputs'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
echo "${{ toJSON(inputs) }}"
|
||||
|
||||
- name: 'Calculate Versions and SHAs'
|
||||
id: 'versions'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
set -e
|
||||
STABLE_JSON=$(node scripts/get-release-version.js --type=stable ${{ github.event.inputs.stable_version_override && format('--stable_version_override={0}', github.event.inputs.stable_version_override) || '' }})
|
||||
PREVIEW_JSON=$(node scripts/get-release-version.js --type=preview ${{ github.event.inputs.preview_version_override && format('--preview_version_override={0}', github.event.inputs.preview_version_override) || '' }})
|
||||
NIGHTLY_JSON=$(node scripts/get-release-version.js --type=promote-nightly)
|
||||
echo "STABLE_JSON_COMMAND=node scripts/get-release-version.js --type=stable ${{ github.event.inputs.stable_version_override && format('--stable_version_override={0}', github.event.inputs.stable_version_override) || '' }}"
|
||||
echo "PREVIEW_JSON_COMMAND=node scripts/get-release-version.js --type=preview ${{ github.event.inputs.preview_version_override && format('--preview_version_override={0}', github.event.inputs.preview_version_override) || '' }}"
|
||||
echo "NIGHTLY_JSON_COMMAND=node scripts/get-release-version.js --type=promote-nightly"
|
||||
echo "STABLE_JSON: ${STABLE_JSON}"
|
||||
echo "PREVIEW_JSON: ${PREVIEW_JSON}"
|
||||
echo "NIGHTLY_JSON: ${NIGHTLY_JSON}"
|
||||
echo "STABLE_VERSION=$(echo "${STABLE_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
|
||||
# shellcheck disable=SC1083
|
||||
echo "STABLE_SHA=$(git rev-parse "$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)"^{commit})" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_STABLE_TAG=$(echo "${STABLE_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIEW_VERSION=$(echo "${PREVIEW_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
|
||||
# shellcheck disable=SC1083
|
||||
echo "PREVIEW_SHA=$(git rev-parse '${{ github.event.inputs.ref }}'^{commit})" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_PREVIEW_TAG=$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "NEXT_NIGHTLY_VERSION=$(echo "${NIGHTLY_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_NIGHTLY_TAG=$(echo "${NIGHTLY_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
CURRENT_NIGHTLY_TAG=$(git describe --tags --abbrev=0 --match="*nightly*")
|
||||
echo "CURRENT_NIGHTLY_TAG=${CURRENT_NIGHTLY_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
echo "NEXT_SHA=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Display Pending Updates'
|
||||
run: |
|
||||
echo "Pending Changes, Next Versions:"
|
||||
echo " Nightly: ${{ steps.versions.outputs.NEXT_NIGHTLY_VERSION }}"
|
||||
echo " Preview: ${{ steps.versions.outputs.PREVIEW_VERSION }}"
|
||||
echo " Stable: ${{ steps.versions.outputs.STABLE_VERSION }}"
|
||||
echo ""
|
||||
echo "Relevant SHAs:"
|
||||
echo " Stable: Will be cut from: : ${{ steps.versions.outputs.STABLE_SHA }} / ${{ steps.versions.outputs.PREVIOUS_STABLE_TAG }}"
|
||||
echo " Preview will be cut from: : ${{ steps.versions.outputs.PREVIEW_SHA }} (${{ github.event.inputs.ref }})". Users last saw preview as: ${{ steps.versions.outputs.PREVIOUS_PREVIEW_TAG }}
|
||||
echo " Nightly will be udpated in : ${{ steps.versions.outputs.NEXT_SHA }} (${{ github.event.inputs.ref }}). Previous nightly tag: ${{ steps.versions.outputs.PREVIOUS_NIGHTLY_TAG }}"
|
||||
|
||||
test:
|
||||
name: 'Test ${{ matrix.channel }}'
|
||||
needs: 'calculate-versions'
|
||||
runs-on: 'ubuntu-latest'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- channel: 'stable'
|
||||
sha: '${{ needs.calculate-versions.outputs.STABLE_SHA }}'
|
||||
- channel: 'preview'
|
||||
sha: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}'
|
||||
- channel: 'nightly'
|
||||
sha: '${{ github.event.inputs.ref }}'
|
||||
steps:
|
||||
- name: 'Checkout Ref'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
- name: 'Checkout correct SHA'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ matrix.sha }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{github.event.inputs.force_skip_tests != 'true'}}"
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
working-directory: './release'
|
||||
|
||||
publish-preview:
|
||||
name: 'Publish preview'
|
||||
needs: ['calculate-versions', 'test']
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout Ref'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
- name: 'Checkout correct SHA'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Publish Release'
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
|
||||
release-tag: 'v${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
|
||||
npm-tag: 'preview'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_PREVIEW_TAG }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The promote-release workflow failed during preview publish. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'kind/bug,release-failure,priority/p0'
|
||||
|
||||
publish-stable:
|
||||
name: 'Publish stable'
|
||||
needs: ['calculate-versions', 'test', 'publish-preview']
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout Ref'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
- name: 'Checkout correct SHA'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ needs.calculate-versions.outputs.STABLE_SHA }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Publish Release'
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
|
||||
release-tag: 'v${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
|
||||
npm-tag: 'latest'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_STABLE_TAG }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The promote-release workflow failed during stable publish. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'kind/bug,release-failure,priority/p0'
|
||||
|
||||
nightly-pr:
|
||||
name: 'Create Nightly PR'
|
||||
needs: ['publish-stable', 'calculate-versions']
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout Ref'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Configure Git User'
|
||||
run: |-
|
||||
git config user.name "gemini-cli-robot"
|
||||
git config user.email "gemini-cli-robot@google.com"
|
||||
|
||||
- name: 'Create and switch to a new branch'
|
||||
id: 'release_branch'
|
||||
run: |
|
||||
BRANCH_NAME="chore/nightly-version-bump-${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Update package versions'
|
||||
run: 'npm run release:version "${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"'
|
||||
|
||||
- name: 'Commit and Push package versions'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
run: |-
|
||||
git add package.json packages/*/package.json
|
||||
if [ -f package-lock.json ]; then
|
||||
git add package-lock.json
|
||||
fi
|
||||
git commit -m "chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"
|
||||
if [[ "${DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push --set-upstream origin "${BRANCH_NAME}"
|
||||
else
|
||||
echo "Dry run enabled. Skipping push."
|
||||
fi
|
||||
|
||||
- name: 'Create and Merge Pull Request'
|
||||
uses: './.github/actions/create-pull-request'
|
||||
with:
|
||||
branch-name: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
pr-title: 'chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
pr-body: 'Automated version bump to prepare for the next nightly release.'
|
||||
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The promote-release workflow failed during nightly PR creation. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'kind/bug,release-failure,priority/p0'
|
||||
@@ -0,0 +1,164 @@
|
||||
name: 'Release: Rollback change'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
rollback_origin:
|
||||
description: 'The package version to rollback FROM and delete (e.g., 0.5.0-preview-2)'
|
||||
required: true
|
||||
type: 'string'
|
||||
rollback_destination:
|
||||
description: 'The package version to rollback TO (e.g., 0.5.0-preview-2). This version must already exist on the npm registry.'
|
||||
required: false
|
||||
type: 'string'
|
||||
channel:
|
||||
description: 'The npm dist-tag to apply to rollback_destination (e.g., latest, preview, nightly). REQUIRED IF rollback_destination is set.'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'latest'
|
||||
- 'preview'
|
||||
- 'nightly'
|
||||
- 'dev'
|
||||
default: 'dev'
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to run from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: 'https://wombat-dressing-room.appspot.com'
|
||||
scope: '@google'
|
||||
|
||||
- name: 'Get Origin Version Tag'
|
||||
id: 'origin_tag'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
TAG_VALUE="v${{ github.event.inputs.rollback_origin }}"
|
||||
echo "ORIGIN_TAG=$TAG_VALUE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Get Origin Commit Hash'
|
||||
id: 'origin_hash'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "ORIGIN_HASH=$(git rev-parse ${{ steps.origin_tag.outputs.ORIGIN_TAG }})" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Change tag'
|
||||
if: "${{ github.event.inputs.rollback_destination != '' }}"
|
||||
uses: './.github/actions/tag-npm-release'
|
||||
with:
|
||||
channel: '${{ github.event.inputs.channel }}'
|
||||
version: '${{ github.event.inputs.rollback_destination }}'
|
||||
dry-run: '${{ github.event.inputs.dry-run }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
|
||||
- name: 'Deprecate Cli Npm Package'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm deprecate @google/gemini-cli@${{ github.event.inputs.rollback_origin }} "This version has been rolled back."
|
||||
|
||||
- name: 'Deprecate Core Npm Package'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm deprecate @google/gemini-core@${{ github.event.inputs.rollback_origin }} "This version has been rolled back."
|
||||
|
||||
- name: 'Delete Github Release'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
gh release delete '${{ steps.origin_tag.outputs.ORIGIN_TAG }}' --yes
|
||||
|
||||
- name: 'Verify Origin Release Deletion'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
TARGET_TAG="${{ steps.origin_tag.outputs.ORIGIN_TAG }}"
|
||||
RELEASE_TAG=$(gh release view "$TARGET_TAG" --json tagName --jq .tagName)
|
||||
if [ "$RELEASE_TAG" = "$TARGET_TAG" ]; then
|
||||
echo '❌ Failed to delete release with tag ${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
echo '❌ This means the release was not deleted, and the workflow should fail.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Add Rollback Tag'
|
||||
id: 'rollback_tag'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
ROLLBACK_TAG_NAME="${{ steps.origin_tag.outputs.ORIGIN_TAG }}-rollback"
|
||||
echo "ROLLBACK_TAG=$ROLLBACK_TAG_NAME" >> "$GITHUB_OUTPUT"
|
||||
git tag "$ROLLBACK_TAG_NAME" "${{ steps.origin_hash.outputs.ORIGIN_HASH }}"
|
||||
git push origin --tags
|
||||
|
||||
- name: 'Verify Rollback Tag Added'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
TARGET_TAG="${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}"
|
||||
TARGET_HASH="${{ steps.origin_hash.outputs.ORIGIN_HASH }}"
|
||||
ROLLBACK_COMMIT=$(git rev-parse -q --verify "$TARGET_TAG")
|
||||
if [ "$ROLLBACK_COMMIT" != "$TARGET_HASH" ]; then
|
||||
echo '❌ Failed to add tag $TARGET_TAG to commit $TARGET_HASH'
|
||||
echo '❌ This means the tag was not added, and the workflow should fail.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Log Dry run'
|
||||
if: "${{ github.event.inputs.dry-run == 'true' }}"
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "
|
||||
Inputs:
|
||||
- rollback_origin: '${{ github.event.inputs.rollback_origin }}'
|
||||
- rollback_destination: '${{ github.event.inputs.rollback_destination }}'
|
||||
- channel: '${{ github.event.inputs.channel }}'
|
||||
- ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
Outputs:
|
||||
- ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
- ORIGIN_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
|
||||
- ROLLBACK_TAG: '${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}'
|
||||
|
||||
Would have npm deprecate @google/gemini-cli@${{ github.event.inputs.rollback_origin }} and @google/gemini-core@${{ github.event.inputs.rollback_origin }}
|
||||
Would have deleted the github release with tag ${{ steps.origin_tag.outputs.ORIGIN_TAG }}
|
||||
Would have added tag ${{ steps.origin_tag.outputs.ORIGIN_TAG }}-rollback to ${{ steps.origin_hash.outputs.ORIGIN_HASH }}
|
||||
"
|
||||
@@ -0,0 +1,49 @@
|
||||
name: 'Release Sandbox'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
dry-run:
|
||||
description: 'Whether this is a dry run.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref || github.sha }}'
|
||||
fetch-depth: 0
|
||||
- name: 'Push'
|
||||
uses: './.github/actions/push-sandbox'
|
||||
with:
|
||||
dockerhub-username: '${{ secrets.DOCKER_SERVICE_ACCOUNT_NAME }}'
|
||||
dockerhub-token: '${{ secrets.DOCKER_SERVICE_ACCOUNT_KEY }}'
|
||||
github-actor: '${{ github.actor }}'
|
||||
github-secret: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-sha: '${{ github.sha }}'
|
||||
github-ref-name: '${{github.event.inputs.ref}}'
|
||||
dry-run: '${{ github.event.inputs.dry-run }}'
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry-run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Sandbox Release Failed on $(date +'%Y-%m-%d')' \
|
||||
--body 'The sandbox-release workflow failed. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'kind/bug,release-failure,priority/p0'
|
||||
@@ -1,231 +0,0 @@
|
||||
name: 'Release'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Runs every day at midnight UTC for the nightly release.
|
||||
- cron: '0 0 * * *'
|
||||
# Runs every Tuesday at 23:59 UTC for the preview release.
|
||||
- cron: '59 23 * * 2'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'The version to release (e.g., v0.1.11). Required for manual patch releases.'
|
||||
required: false # Not required for scheduled runs
|
||||
type: 'string'
|
||||
ref:
|
||||
description: 'The branch or ref (full git sha) to release from.'
|
||||
required: true
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
dry_run:
|
||||
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
create_nightly_release:
|
||||
description: 'Auto apply the nightly release tag, input version is ignored.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
create_preview_release:
|
||||
description: 'Auto apply the preview release tag, input version is ignored.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
force_skip_tests:
|
||||
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment:
|
||||
name: 'production-release'
|
||||
url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
id-token: 'write'
|
||||
issues: 'write' # For creating issues on failure
|
||||
outputs:
|
||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref || github.sha }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Set booleans for simplified logic'
|
||||
env:
|
||||
CREATE_NIGHTLY_RELEASE: '${{ github.event.inputs.create_nightly_release }}'
|
||||
CREATE_PREVIEW_RELEASE: '${{ github.event.inputs.create_preview_release }}'
|
||||
EVENT_NAME: '${{ github.event_name }}'
|
||||
CRON: '${{ github.event.schedule }}'
|
||||
DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}'
|
||||
id: 'vars'
|
||||
run: |-
|
||||
is_nightly="false"
|
||||
if [[ "${CRON}" == "0 0 * * *" || "${CREATE_NIGHTLY_RELEASE}" == "true" ]]; then
|
||||
is_nightly="true"
|
||||
fi
|
||||
echo "is_nightly=${is_nightly}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
is_preview="false"
|
||||
if [[ "${CRON}" == "59 23 * * 2" || "${CREATE_PREVIEW_RELEASE}" == "true" ]]; then
|
||||
is_preview="true"
|
||||
fi
|
||||
echo "is_preview=${is_preview}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
is_dry_run="false"
|
||||
if [[ "${DRY_RUN_INPUT}" == "true" ]]; then
|
||||
is_dry_run="true"
|
||||
fi
|
||||
echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
|
||||
- name: 'Get the version'
|
||||
id: 'version'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}'
|
||||
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
|
||||
MANUAL_VERSION: '${{ inputs.version }}'
|
||||
run: |-
|
||||
VERSION_JSON="$(node scripts/get-release-version.js)"
|
||||
echo "RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
|
||||
echo "NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: |-
|
||||
${{ github.event.inputs.force_skip_tests != 'true' }}
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
run: |-
|
||||
npm run preflight
|
||||
npm run test:integration:sandbox:none
|
||||
npm run test:integration:sandbox:docker
|
||||
|
||||
- name: 'Configure Git User'
|
||||
run: |-
|
||||
git config user.name "gemini-cli-robot"
|
||||
git config user.email "gemini-cli-robot@google.com"
|
||||
|
||||
- name: 'Create and switch to a release branch'
|
||||
id: 'release_branch'
|
||||
env:
|
||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
run: |-
|
||||
BRANCH_NAME="release/${RELEASE_TAG}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Update package versions'
|
||||
env:
|
||||
RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}'
|
||||
run: |-
|
||||
npm run release:version "${RELEASE_VERSION}"
|
||||
|
||||
- name: 'Commit and Conditionally Push package versions'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
|
||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
run: |-
|
||||
git add package.json package-lock.json packages/*/package.json
|
||||
git commit -m "chore(release): ${RELEASE_TAG}"
|
||||
if [[ "${IS_DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
|
||||
else
|
||||
echo "Dry run enabled. Skipping push."
|
||||
fi
|
||||
|
||||
- name: 'Build and Prepare Packages'
|
||||
run: |-
|
||||
npm run build:packages
|
||||
npm run prepare:package
|
||||
|
||||
- name: 'Configure npm for publishing'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: 'https://wombat-dressing-room.appspot.com'
|
||||
scope: '@google'
|
||||
|
||||
- name: 'Publish @google/gemini-cli-core'
|
||||
env:
|
||||
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
|
||||
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
NPM_TAG: '${{ steps.version.outputs.NPM_TAG }}'
|
||||
run: |-
|
||||
npm publish \
|
||||
--dry-run="${IS_DRY_RUN}" \
|
||||
--workspace="@google/gemini-cli-core" \
|
||||
--tag="${NPM_TAG}"
|
||||
|
||||
- name: 'Install latest core package'
|
||||
if: |-
|
||||
${{ steps.vars.outputs.is_dry_run == 'false' }}
|
||||
env:
|
||||
RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}'
|
||||
run: |-
|
||||
npm install "@google/gemini-cli-core@${RELEASE_VERSION}" \
|
||||
--workspace="@google/gemini-cli" \
|
||||
--save-exact
|
||||
|
||||
- name: 'Publish @google/gemini-cli'
|
||||
env:
|
||||
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
|
||||
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
NPM_TAG: '${{ steps.version.outputs.NPM_TAG }}'
|
||||
run: |-
|
||||
npm publish \
|
||||
--dry-run="${IS_DRY_RUN}" \
|
||||
--workspace="@google/gemini-cli" \
|
||||
--tag="${NPM_TAG}"
|
||||
|
||||
- name: 'Create GitHub Release and Tag'
|
||||
if: |-
|
||||
${{ steps.vars.outputs.is_dry_run == 'false' }}
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
PREVIOUS_TAG: '${{ steps.version.outputs.PREVIOUS_TAG }}'
|
||||
run: |-
|
||||
gh release create "${RELEASE_TAG}" \
|
||||
bundle/gemini.js \
|
||||
--target "$RELEASE_BRANCH" \
|
||||
--title "Release ${RELEASE_TAG}" \
|
||||
--notes-start-tag "$PREVIOUS_TAG" \
|
||||
--generate-notes
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: |-
|
||||
${{ failure() }}
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }} || "N/A"'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |-
|
||||
gh issue create \
|
||||
--title "Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
|
||||
--body "The release workflow failed. See the full run for details: ${DETAILS_URL}" \
|
||||
--label "kind/bug,release-failure"
|
||||
@@ -8,7 +8,13 @@ on:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: 'ubuntu-latest'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner:
|
||||
- 'ubuntu-latest' # GitHub-hosted
|
||||
- 'self-hosted' # GHA on GCP runners
|
||||
runs-on: '${{ matrix.runner }}'
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
permissions:
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: 'Verify NPM release tag'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'The expected Gemini binary version that should be released (e.g., 0.5.0-preview-2).'
|
||||
required: true
|
||||
type: 'string'
|
||||
npm-package:
|
||||
description: 'NPM package to verify'
|
||||
required: true
|
||||
type: 'string'
|
||||
default: '@google/gemini-cli@latest'
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
- name: 'Verify release'
|
||||
uses: './.github/actions/verify-release'
|
||||
with:
|
||||
npm-package: '${{github.event.inputs.npm-package}}'
|
||||
expected-version: '${{github.event.inputs.version}}'
|
||||
ref: '${{github.event.inputs.ref}}'
|
||||
+12
-2
@@ -3,14 +3,21 @@
|
||||
.env~
|
||||
|
||||
# gemini-cli settings
|
||||
.gemini/
|
||||
!gemini/config.yaml
|
||||
# We want to keep the .gemini in the root of the repo and ignore any .gemini
|
||||
# in subdirectories. In our root .gemini we want to allow for version control
|
||||
# for subcommands.
|
||||
**/.gemini/
|
||||
!/.gemini/
|
||||
.gemini/*
|
||||
!.gemini/config.yaml
|
||||
!.gemini/commands/
|
||||
|
||||
# Note: .gemini-clipboard/ is NOT in gitignore so Gemini can access pasted images
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
bower_components
|
||||
package-lock.json
|
||||
|
||||
# Editors
|
||||
.idea
|
||||
@@ -43,3 +50,6 @@ packages/vscode-ide-companion/*.vsix
|
||||
|
||||
# GHA credentials
|
||||
gha-creds-*.json
|
||||
|
||||
# Log files
|
||||
patch_output.log
|
||||
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
npm run pre-commit || {
|
||||
echo ''
|
||||
echo '===================================================='
|
||||
echo 'pre-commit checks failed. in case of emergency, run:'
|
||||
echo ''
|
||||
echo 'git commit --no-verify'
|
||||
echo '===================================================='
|
||||
exit 1
|
||||
}
|
||||
Vendored
+2
-1
@@ -12,5 +12,6 @@
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
},
|
||||
"vitest.disableWorkspaceWarning": true
|
||||
}
|
||||
|
||||
@@ -86,3 +86,4 @@ ignore:
|
||||
- 'thirdparty/'
|
||||
- 'third_party/'
|
||||
- 'vendor/'
|
||||
- 'node_modules/'
|
||||
|
||||
+9
-1
@@ -31,6 +31,14 @@ All submissions, including submissions by project members, require review. We
|
||||
use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests)
|
||||
for this purpose.
|
||||
|
||||
### Self Assigning Issues
|
||||
|
||||
If you're looking for an issue to work on, check out our list of issues that are labeled ["help wanted"](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue+state%3Aopen+label%3A%22help+wanted%22).
|
||||
|
||||
To assign an issue to yourself, simply add a comment with the text `/assign`. The comment must contain only that text and nothing else. This command will assign the issue to you, provided it is not already assigned.
|
||||
|
||||
Please note that you can have a maximum of 3 issues assigned to you at any given time.
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
To help us review and merge your PRs quickly, please follow these guidelines. PRs that do not meet these standards may be closed.
|
||||
@@ -136,7 +144,7 @@ To start the Gemini CLI from the source code (after building), run the following
|
||||
npm start
|
||||
```
|
||||
|
||||
If you'd like to run the source build outside of the gemini-cli folder you can utilize `npm link path/to/gemini-cli/packages/cli` (see: [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) or `alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`
|
||||
If you'd like to run the source build outside of the gemini-cli folder, you can utilize `npm link path/to/gemini-cli/packages/cli` (see: [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) or `alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`
|
||||
|
||||
### Running Tests
|
||||
|
||||
|
||||
+4
-4
@@ -40,11 +40,11 @@ ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
||||
USER node
|
||||
|
||||
# install gemini-cli and clean up
|
||||
COPY packages/cli/dist/google-gemini-cli-*.tgz /usr/local/share/npm-global/gemini-cli.tgz
|
||||
COPY packages/core/dist/google-gemini-cli-core-*.tgz /usr/local/share/npm-global/gemini-core.tgz
|
||||
RUN npm install -g /usr/local/share/npm-global/gemini-cli.tgz /usr/local/share/npm-global/gemini-core.tgz \
|
||||
COPY packages/cli/dist/google-gemini-cli-*.tgz /tmp/gemini-cli.tgz
|
||||
COPY packages/core/dist/google-gemini-cli-core-*.tgz /tmp/gemini-core.tgz
|
||||
RUN npm install -g /tmp/gemini-cli.tgz /tmp/gemini-core.tgz \
|
||||
&& npm cache clean --force \
|
||||
&& rm -f /usr/local/share/npm-global/gemini-{cli,core}.tgz
|
||||
&& rm -f /tmp/gemini-{cli,core}.tgz
|
||||
|
||||
# default entrypoint when none specified
|
||||
CMD ["gemini"]
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 Google LLC
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -199,4 +199,4 @@
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
limitations under the License.
|
||||
@@ -1,6 +1,7 @@
|
||||
# Gemini CLI
|
||||
|
||||
[](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml)
|
||||
[](https://github.com/google-gemini/gemini-cli/actions/workflows/e2e.yml)
|
||||
[](https://www.npmjs.com/package/@google/gemini-cli)
|
||||
[](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
|
||||
|
||||
@@ -106,18 +107,24 @@ Integrate Gemini CLI directly into your GitHub workflows with [**Gemini CLI GitH
|
||||
|
||||
Choose the authentication method that best fits your needs:
|
||||
|
||||
### Option 1: OAuth login (Using your Google Account)
|
||||
### Option 1: Login with Google (OAuth login using your Google Account)
|
||||
|
||||
**✨ Best for:** Individual developers as well as anyone who has a Gemini Code Assist License. (see [quota limits and terms of service](https://cloud.google.com/gemini/docs/quotas) for details)
|
||||
**✨ Best for:**
|
||||
|
||||
- Individual developers.
|
||||
- Google AI Pro and AI Ultra subscribers.
|
||||
- Anyone who has a Gemini Code Assist license.
|
||||
|
||||
_See [quota limits and terms of service](https://cloud.google.com/gemini/docs/quotas) for details._
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- **Free tier**: 60 requests/min and 1,000 requests/day
|
||||
- **Gemini 2.5 Pro** with 1M token context window
|
||||
- **Free tier** with 60 requests/min and 1,000 requests/day
|
||||
- **Gemini 2.5 Pro and Flash** with 1M token context window
|
||||
- **No API key management** - just sign in with your Google account
|
||||
- **Automatic updates** to latest models
|
||||
- **Automatic updates** to our latest models
|
||||
|
||||
#### Start Gemini CLI, then choose OAuth and follow the browser authentication flow when prompted
|
||||
#### Start Gemini CLI, then choose _Login with Google_ and follow the browser authentication flow when prompted
|
||||
|
||||
```bash
|
||||
gemini
|
||||
@@ -190,10 +197,19 @@ gemini -m gemini-2.5-flash
|
||||
|
||||
#### Non-interactive mode for scripts
|
||||
|
||||
Get a simple text response:
|
||||
|
||||
```bash
|
||||
gemini -p "Explain the architecture of this codebase"
|
||||
```
|
||||
|
||||
For more advanced scripting, including how to parse JSON and handle errors, use
|
||||
the `--output-format json` flag to get structured output:
|
||||
|
||||
```bash
|
||||
gemini -p "Explain the architecture of this codebase" --output-format json
|
||||
```
|
||||
|
||||
### Quick Examples
|
||||
|
||||
#### Start a new project
|
||||
|
||||
@@ -38,8 +38,10 @@ Add the following key to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"checkpointing": {
|
||||
"enabled": true
|
||||
"general": {
|
||||
"checkpointing": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+29
-21
@@ -1,30 +1,35 @@
|
||||
# Authentication Setup
|
||||
|
||||
The Gemini CLI requires you to authenticate with Google's AI services. On initial startup you'll need to configure **one** of the following authentication methods:
|
||||
Gemini CLI requires you to authenticate with Google's AI services. On initial startup you'll need to configure **one** of the following authentication methods:
|
||||
|
||||
1. **Login with Google (Gemini Code Assist):**
|
||||
- Use this option to log in with your Google account.
|
||||
- During initial startup, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs.
|
||||
- Note that the web login must be done in a browser that can communicate with the machine Gemini CLI is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on).
|
||||
- <a id="workspace-gca">Users may have to specify a GOOGLE_CLOUD_PROJECT if:</a>
|
||||
1. You have a Google Workspace account. Google Workspace is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school.
|
||||
1. You have received a Gemini Code Assist license through the [Google Developer Program](https://developers.google.com/program/plans-and-pricing) (including qualified Google Developer Experts)
|
||||
1. You have been assigned a license to a current Gemini Code Assist standard or enterprise subscription.
|
||||
1. You are using the product outside the [supported regions](https://developers.google.com/gemini-code-assist/resources/available-locations) for free individual usage.
|
||||
1. You are a Google account holder under the age of 18
|
||||
- If you fall into one of these categories, you must first configure a Google Cloud Project ID to use, [enable the Gemini for Cloud API](https://cloud.google.com/gemini/docs/discover/set-up-gemini#enable-api) and [configure access permissions](https://cloud.google.com/gemini/docs/discover/set-up-gemini#grant-iam).
|
||||
1. **Login with Google**
|
||||
1. **Google AI Pro and AI Ultra subscribers**
|
||||
- Use this option to log in with your Google account that you use with Google AI Pro and Ultra.
|
||||
- During initial startup, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs.
|
||||
- Note that the web login must be done in a browser that can communicate with the machine Gemini CLI is being run from. (Specifically, the browser will be redirected to a localhost URL that Gemini CLI will be listening on.)
|
||||
2. **Gemini Code Assist:**
|
||||
- Use this option to log in with your Google account.
|
||||
- During initial startup, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs.
|
||||
- Note that the web login must be done in a browser that can communicate with the machine Gemini CLI is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on.)
|
||||
- <a id="workspace-gca">Users may have to specify a GOOGLE_CLOUD_PROJECT if:</a>
|
||||
1. You have a Google Workspace account. Google Workspace is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school.
|
||||
1. You have received a Gemini Code Assist license through the [Google Developer Program](https://developers.google.com/program/plans-and-pricing) (including qualified Google Developer Experts).
|
||||
1. You have been assigned a license to a current Gemini Code Assist standard or enterprise subscription.
|
||||
1. You are using the product outside the [supported regions](https://developers.google.com/gemini-code-assist/resources/available-locations) for free individual usage.
|
||||
1. You are a Google account holder under the age of 18.
|
||||
- If you fall into one of these categories, you must first configure a Google Cloud Project ID to use, [enable the Gemini for Cloud API](https://cloud.google.com/gemini/docs/discover/set-up-gemini#enable-api) and [configure access permissions](https://cloud.google.com/gemini/docs/discover/set-up-gemini#grant-iam).
|
||||
|
||||
You can temporarily set the environment variable in your current shell session using the following command:
|
||||
You can temporarily set the environment variable in your current shell session using the following command:
|
||||
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your [.env file](#persisting-environment-variables-with-env-files) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
2. **<a id="gemini-api-key"></a>Gemini API key:**
|
||||
- Obtain your API key from Google AI Studio: [https://aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
|
||||
@@ -171,3 +176,6 @@ following authentication methods if available:
|
||||
- Ensure the `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` environment variables are set.
|
||||
|
||||
If none of these environment variables are set in a non-interactive session, the CLI will exit with an error.
|
||||
|
||||
For comprehensive guidance on using Gemini CLI programmatically and in
|
||||
automation workflows, see the [Headless Mode Guide](../headless.md).
|
||||
|
||||
+15
-2
@@ -9,7 +9,7 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
### Built-in Commands
|
||||
|
||||
- **`/bug`**
|
||||
- **Description:** File an issue about Gemini CLI. By default, the issue is filed within the GitHub repository for Gemini CLI. The string you enter after `/bug` will become the headline for the bug being filed. The default `/bug` behavior can be modified using the `bugCommand` setting in your `.gemini/settings.json` files.
|
||||
- **Description:** File an issue about Gemini CLI. By default, the issue is filed within the GitHub repository for Gemini CLI. The string you enter after `/bug` will become the headline for the bug being filed. The default `/bug` behavior can be modified using the `advanced.bugCommand` setting in your `.gemini/settings.json` files.
|
||||
|
||||
- **`/chat`**
|
||||
- **Description:** Save and resume conversation history for branching conversation state interactively, or resuming a previous state from a later session.
|
||||
@@ -30,6 +30,9 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **`delete`**
|
||||
- **Description:** Deletes a saved conversation checkpoint.
|
||||
- **Usage:** `/chat delete <tag>`
|
||||
- **`share`**
|
||||
- **Description** Writes the current conversation to a provided Markdown or JSON file.
|
||||
- **Usage** `/chat share file.md` or `/chat share file.json`. If no filename is provided, then the CLI will generate one.
|
||||
|
||||
- **`/clear`**
|
||||
- **Description:** Clear the terminal screen, including the visible session history and scrollback within the CLI. The underlying session data (for history recall) might be preserved depending on the exact implementation, but the visual display is cleared.
|
||||
@@ -380,6 +383,16 @@ That's it! You can now run your command in the CLI. First, you might add a file
|
||||
|
||||
Gemini CLI will then execute the multi-line prompt defined in your TOML file.
|
||||
|
||||
## Input Prompt Shortcuts
|
||||
|
||||
These shortcuts apply directly to the input prompt for text manipulation.
|
||||
|
||||
- **Undo:**
|
||||
- **Keyboard shortcut:** Press **Ctrl+z** to undo the last action in the input prompt.
|
||||
|
||||
- **Redo:**
|
||||
- **Keyboard shortcut:** Press **Ctrl+Shift+Z** to redo the last undone action in the input prompt.
|
||||
|
||||
## At commands (`@`)
|
||||
|
||||
At commands are used to include the content of files or directories as part of your prompt to Gemini. These commands include git-aware filtering.
|
||||
@@ -395,7 +408,7 @@ At commands are used to include the content of files or directories as part of y
|
||||
- If a path to a directory is provided, the command attempts to read the content of files within that directory and any subdirectories.
|
||||
- Spaces in paths should be escaped with a backslash (e.g., `@My\ Documents/file.txt`).
|
||||
- The command uses the `read_many_files` tool internally. The content is fetched and then inserted into your query before being sent to the Gemini model.
|
||||
- **Git-aware filtering:** By default, git-ignored files (like `node_modules/`, `dist/`, `.env`, `.git/`) are excluded. This behavior can be changed via the `fileFiltering` settings.
|
||||
- **Git-aware filtering:** By default, git-ignored files (like `node_modules/`, `dist/`, `.env`, `.git/`) are excluded. This behavior can be changed via the `context.fileFiltering` settings.
|
||||
- **File types:** The command is intended for text-based files. While it might attempt to read any file, binary files or very large files might be skipped or truncated by the underlying `read_many_files` tool to ensure performance and relevance. The tool indicates if files were skipped.
|
||||
- **Output:** The CLI will show a tool call message indicating that `read_many_files` was used, along with a message detailing the status and the path(s) that were processed.
|
||||
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
# Gemini CLI Configuration
|
||||
|
||||
**Note on Deprecated Configuration Format**
|
||||
|
||||
This document describes the legacy v1 format for the `settings.json` file. This format is now deprecated.
|
||||
|
||||
- The new format will be supported in the stable release starting **[09/10/25]**.
|
||||
- Automatic migration from the old format to the new format will begin on **[09/17/25]**.
|
||||
|
||||
For details on the new, recommended format, please see the [current Configuration documentation](./configuration.md).
|
||||
|
||||
Gemini CLI offers several ways to configure its behavior, including environment variables, command-line arguments, and settings files. This document outlines the different configuration methods and available settings.
|
||||
|
||||
## Configuration layers
|
||||
|
||||
Configuration is applied in the following order of precedence (lower numbers are overridden by higher numbers):
|
||||
|
||||
1. **Default values:** Hardcoded defaults within the application.
|
||||
2. **System defaults file:** System-wide default settings that can be overridden by other settings files.
|
||||
3. **User settings file:** Global settings for the current user.
|
||||
4. **Project settings file:** Project-specific settings.
|
||||
5. **System settings file:** System-wide settings that override all other settings files.
|
||||
6. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files.
|
||||
7. **Command-line arguments:** Values passed when launching the CLI.
|
||||
|
||||
## Settings files
|
||||
|
||||
Gemini CLI uses JSON settings files for persistent configuration. There are four locations for these files:
|
||||
|
||||
- **System defaults file:**
|
||||
- **Location:** `/etc/gemini-cli/system-defaults.json` (Linux), `C:\ProgramData\gemini-cli\system-defaults.json` (Windows) or `/Library/Application Support/GeminiCli/system-defaults.json` (macOS). The path can be overridden using the `GEMINI_CLI_SYSTEM_DEFAULTS_PATH` environment variable.
|
||||
- **Scope:** Provides a base layer of system-wide default settings. These settings have the lowest precedence and are intended to be overridden by user, project, or system override settings.
|
||||
- **User settings file:**
|
||||
- **Location:** `~/.gemini/settings.json` (where `~` is your home directory).
|
||||
- **Scope:** Applies to all Gemini CLI sessions for the current user. User settings override system defaults.
|
||||
- **Project settings file:**
|
||||
- **Location:** `.gemini/settings.json` within your project's root directory.
|
||||
- **Scope:** Applies only when running Gemini CLI from that specific project. Project settings override user settings and system defaults.
|
||||
- **System settings file:**
|
||||
- **Location:** `/etc/gemini-cli/settings.json` (Linux), `C:\ProgramData\gemini-cli\settings.json` (Windows) or `/Library/Application Support/GeminiCli/settings.json` (macOS). The path can be overridden using the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` environment variable.
|
||||
- **Scope:** Applies to all Gemini CLI sessions on the system, for all users. System settings act as overrides, taking precedence over all other settings files. May be useful for system administrators at enterprises to have controls over users' Gemini CLI setups.
|
||||
|
||||
**Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`.
|
||||
|
||||
> **Note for Enterprise Users:** For guidance on deploying and managing Gemini CLI in a corporate environment, please see the [Enterprise Configuration](./enterprise.md) documentation.
|
||||
|
||||
### The `.gemini` directory in your project
|
||||
|
||||
In addition to a project settings file, a project's `.gemini` directory can contain other project-specific files related to Gemini CLI's operation, such as:
|
||||
|
||||
- [Custom sandbox profiles](#sandboxing) (e.g., `.gemini/sandbox-macos-custom.sb`, `.gemini/sandbox.Dockerfile`).
|
||||
|
||||
### Available settings in `settings.json`:
|
||||
|
||||
- **`contextFileName`** (string or array of strings):
|
||||
- **Description:** Specifies the filename for context files (e.g., `GEMINI.md`, `AGENTS.md`). Can be a single filename or a list of accepted filenames.
|
||||
- **Default:** `GEMINI.md`
|
||||
- **Example:** `"contextFileName": "AGENTS.md"`
|
||||
|
||||
- **`bugCommand`** (object):
|
||||
- **Description:** Overrides the default URL for the `/bug` command.
|
||||
- **Default:** `"urlTemplate": "https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}"`
|
||||
- **Properties:**
|
||||
- **`urlTemplate`** (string): A URL that can contain `{title}` and `{info}` placeholders.
|
||||
- **Example:**
|
||||
```json
|
||||
"bugCommand": {
|
||||
"urlTemplate": "https://bug.example.com/new?title={title}&info={info}"
|
||||
}
|
||||
```
|
||||
|
||||
- **`fileFiltering`** (object):
|
||||
- **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools.
|
||||
- **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true`
|
||||
- **Properties:**
|
||||
- **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations.
|
||||
- **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt.
|
||||
- **`disableFuzzySearch`** (boolean): When `true`, disables the fuzzy search capabilities when searching for files, which can improve performance on projects with a large number of files.
|
||||
- **Example:**
|
||||
```json
|
||||
"fileFiltering": {
|
||||
"respectGitIgnore": true,
|
||||
"enableRecursiveFileSearch": false,
|
||||
"disableFuzzySearch": true
|
||||
}
|
||||
```
|
||||
|
||||
### Troubleshooting File Search Performance
|
||||
|
||||
If you are experiencing performance issues with file searching (e.g., with `@` completions), especially in projects with a very large number of files, here are a few things you can try in order of recommendation:
|
||||
|
||||
1. **Use `.geminiignore`:** Create a `.geminiignore` file in your project root to exclude directories that contain a large number of files that you don't need to reference (e.g., build artifacts, logs, `node_modules`). Reducing the total number of files crawled is the most effective way to improve performance.
|
||||
|
||||
2. **Disable Fuzzy Search:** If ignoring files is not enough, you can disable fuzzy search by setting `disableFuzzySearch` to `true` in your `settings.json` file. This will use a simpler, non-fuzzy matching algorithm, which can be faster.
|
||||
|
||||
3. **Disable Recursive File Search:** As a last resort, you can disable recursive file search entirely by setting `enableRecursiveFileSearch` to `false`. This will be the fastest option as it avoids a recursive crawl of your project. However, it means you will need to type the full path to files when using `@` completions.
|
||||
|
||||
- **`coreTools`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to be executed.
|
||||
- **Default:** All tools available for use by the Gemini model.
|
||||
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`.
|
||||
|
||||
- **`allowedTools`** (array of strings):
|
||||
- **Default:** `undefined`
|
||||
- **Description:** A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. The match semantics are the same as `coreTools`.
|
||||
- **Example:** `"allowedTools": ["ShellTool(git status)"]`.
|
||||
|
||||
- **`excludeTools`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command.
|
||||
- **Default**: No tools excluded.
|
||||
- **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
|
||||
- **Security Note:** Command-specific restrictions in
|
||||
`excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands
|
||||
that can be executed.
|
||||
|
||||
- **`allowMCPServers`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of MCP server names that should be made available to the model. This can be used to restrict the set of MCP servers to connect to. Note that this will be ignored if `--allowed-mcp-server-names` is set.
|
||||
- **Default:** All MCP servers are available for use by the Gemini model.
|
||||
- **Example:** `"allowMCPServers": ["myPythonServer"]`.
|
||||
- **Security Note:** This uses simple string matching on MCP server names, which can be modified. If you're a system administrator looking to prevent users from bypassing this, consider configuring the `mcpServers` at the system settings level such that the user will not be able to configure any MCP servers of their own. This should not be used as an airtight security mechanism.
|
||||
|
||||
- **`excludeMCPServers`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of MCP server names that should be excluded from the model. A server listed in both `excludeMCPServers` and `allowMCPServers` is excluded. Note that this will be ignored if `--allowed-mcp-server-names` is set.
|
||||
- **Default**: No MCP servers excluded.
|
||||
- **Example:** `"excludeMCPServers": ["myNodeServer"]`.
|
||||
- **Security Note:** This uses simple string matching on MCP server names, which can be modified. If you're a system administrator looking to prevent users from bypassing this, consider configuring the `mcpServers` at the system settings level such that the user will not be able to configure any MCP servers of their own. This should not be used as an airtight security mechanism.
|
||||
|
||||
- **`autoAccept`** (boolean):
|
||||
- **Description:** Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"autoAccept": true`
|
||||
|
||||
- **`theme`** (string):
|
||||
- **Description:** Sets the visual [theme](./themes.md) for Gemini CLI.
|
||||
- **Default:** `"Default"`
|
||||
- **Example:** `"theme": "GitHub"`
|
||||
|
||||
- **`vimMode`** (boolean):
|
||||
- **Description:** Enables or disables vim mode for input editing. When enabled, the input area supports vim-style navigation and editing commands with NORMAL and INSERT modes. The vim mode status is displayed in the footer and persists between sessions.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"vimMode": true`
|
||||
|
||||
- **`sandbox`** (boolean or string):
|
||||
- **Description:** Controls whether and how to use sandboxing for tool execution. If set to `true`, Gemini CLI uses a pre-built `gemini-cli-sandbox` Docker image. For more information, see [Sandboxing](#sandboxing).
|
||||
- **Default:** `false`
|
||||
- **Example:** `"sandbox": "docker"`
|
||||
|
||||
- **`toolDiscoveryCommand`** (string):
|
||||
- **Description:** Defines a custom shell command for discovering tools from your project. The shell command must return on `stdout` a JSON array of [function declarations](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations). Tool wrappers are optional.
|
||||
- **Default:** Empty
|
||||
- **Example:** `"toolDiscoveryCommand": "bin/get_tools"`
|
||||
|
||||
- **`toolCallCommand`** (string):
|
||||
- **Description:** Defines a custom shell command for calling a specific tool that was discovered using `toolDiscoveryCommand`. The shell command must meet the following criteria:
|
||||
- It must take function `name` (exactly as in [function declaration](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations)) as first command line argument.
|
||||
- It must read function arguments as JSON on `stdin`, analogous to [`functionCall.args`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functioncall).
|
||||
- It must return function output as JSON on `stdout`, analogous to [`functionResponse.response.content`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functionresponse).
|
||||
- **Default:** Empty
|
||||
- **Example:** `"toolCallCommand": "bin/call_tool"`
|
||||
|
||||
- **`mcpServers`** (object):
|
||||
- **Description:** Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Gemini CLI attempts to connect to each configured MCP server to discover available tools. If multiple MCP servers expose a tool with the same name, the tool names will be prefixed with the server alias you defined in the configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note that the system might strip certain schema properties from MCP tool definitions for compatibility. At least one of `command`, `url`, or `httpUrl` must be provided. If multiple are specified, the order of precedence is `httpUrl`, then `url`, then `command`.
|
||||
- **Default:** Empty
|
||||
- **Properties:**
|
||||
- **`<SERVER_NAME>`** (object): The server parameters for the named server.
|
||||
- `command` (string, optional): The command to execute to start the MCP server via standard I/O.
|
||||
- `args` (array of strings, optional): Arguments to pass to the command.
|
||||
- `env` (object, optional): Environment variables to set for the server process.
|
||||
- `cwd` (string, optional): The working directory in which to start the server.
|
||||
- `url` (string, optional): The URL of an MCP server that uses Server-Sent Events (SSE) for communication.
|
||||
- `httpUrl` (string, optional): The URL of an MCP server that uses streamable HTTP for communication.
|
||||
- `headers` (object, optional): A map of HTTP headers to send with requests to `url` or `httpUrl`.
|
||||
- `timeout` (number, optional): Timeout in milliseconds for requests to this MCP server.
|
||||
- `trust` (boolean, optional): Trust this server and bypass all tool call confirmations.
|
||||
- `description` (string, optional): A brief description of the server, which may be used for display purposes.
|
||||
- `includeTools` (array of strings, optional): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default.
|
||||
- `excludeTools` (array of strings, optional): List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded.
|
||||
- **Example:**
|
||||
```json
|
||||
"mcpServers": {
|
||||
"myPythonServer": {
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--port", "8080"],
|
||||
"cwd": "./mcp_tools/python",
|
||||
"timeout": 5000,
|
||||
"includeTools": ["safe_tool", "file_reader"],
|
||||
},
|
||||
"myNodeServer": {
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js"],
|
||||
"cwd": "./mcp_tools/node",
|
||||
"excludeTools": ["dangerous_tool", "file_deleter"]
|
||||
},
|
||||
"myDockerServer": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "-e", "API_KEY", "ghcr.io/foo/bar"],
|
||||
"env": {
|
||||
"API_KEY": "$MY_API_TOKEN"
|
||||
}
|
||||
},
|
||||
"mySseServer": {
|
||||
"url": "http://localhost:8081/events",
|
||||
"headers": {
|
||||
"Authorization": "Bearer $MY_SSE_TOKEN"
|
||||
},
|
||||
"description": "An example SSE-based MCP server."
|
||||
},
|
||||
"myStreamableHttpServer": {
|
||||
"httpUrl": "http://localhost:8082/stream",
|
||||
"headers": {
|
||||
"X-API-Key": "$MY_HTTP_API_KEY"
|
||||
},
|
||||
"description": "An example Streamable HTTP-based MCP server."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`checkpointing`** (object):
|
||||
- **Description:** Configures the checkpointing feature, which allows you to save and restore conversation and file states. See the [Checkpointing documentation](../checkpointing.md) for more details.
|
||||
- **Default:** `{"enabled": false}`
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): When `true`, the `/restore` command is available.
|
||||
|
||||
- **`preferredEditor`** (string):
|
||||
- **Description:** Specifies the preferred editor to use for viewing diffs.
|
||||
- **Default:** `vscode`
|
||||
- **Example:** `"preferredEditor": "vscode"`
|
||||
|
||||
- **`telemetry`** (object)
|
||||
- **Description:** Configures logging and metrics collection for Gemini CLI. For more information, see [Telemetry](../telemetry.md).
|
||||
- **Default:** `{"enabled": false, "target": "local", "otlpEndpoint": "http://localhost:4317", "logPrompts": true}`
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): Whether or not telemetry is enabled.
|
||||
- **`target`** (string): The destination for collected telemetry. Supported values are `local` and `gcp`.
|
||||
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
|
||||
- **`logPrompts`** (boolean): Whether or not to include the content of user prompts in the logs.
|
||||
- **Example:**
|
||||
```json
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "http://localhost:16686",
|
||||
"logPrompts": false
|
||||
}
|
||||
```
|
||||
- **`usageStatisticsEnabled`** (boolean):
|
||||
- **Description:** Enables or disables the collection of usage statistics. See [Usage Statistics](#usage-statistics) for more information.
|
||||
- **Default:** `true`
|
||||
- **Example:**
|
||||
```json
|
||||
"usageStatisticsEnabled": false
|
||||
```
|
||||
|
||||
- **`hideTips`** (boolean):
|
||||
- **Description:** Enables or disables helpful tips in the CLI interface.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
|
||||
```json
|
||||
"hideTips": true
|
||||
```
|
||||
|
||||
- **`hideBanner`** (boolean):
|
||||
- **Description:** Enables or disables the startup banner (ASCII art logo) in the CLI interface.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
|
||||
```json
|
||||
"hideBanner": true
|
||||
```
|
||||
|
||||
- **`maxSessionTurns`** (number):
|
||||
- **Description:** Sets the maximum number of turns for a session. If the session exceeds this limit, the CLI will stop processing and start a new chat.
|
||||
- **Default:** `-1` (unlimited)
|
||||
- **Example:**
|
||||
```json
|
||||
"maxSessionTurns": 10
|
||||
```
|
||||
|
||||
- **`summarizeToolOutput`** (object):
|
||||
- **Description:** Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting.
|
||||
- Note: Currently only the `run_shell_command` tool is supported.
|
||||
- **Default:** `{}` (Disabled by default)
|
||||
- **Example:**
|
||||
```json
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 2000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`excludedProjectEnvVars`** (array of strings):
|
||||
- **Description:** Specifies environment variables that should be excluded from being loaded from project `.env` files. This prevents project-specific environment variables (like `DEBUG=true`) from interfering with gemini-cli behavior. Variables from `.gemini/.env` files are never excluded.
|
||||
- **Default:** `["DEBUG", "DEBUG_MODE"]`
|
||||
- **Example:**
|
||||
```json
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
|
||||
```
|
||||
|
||||
- **`includeDirectories`** (array of strings):
|
||||
- **Description:** Specifies an array of additional absolute or relative paths to include in the workspace context. Missing directories will be skipped with a warning by default. Paths can use `~` to refer to the user's home directory. This setting can be combined with the `--include-directories` command-line flag.
|
||||
- **Default:** `[]`
|
||||
- **Example:**
|
||||
```json
|
||||
"includeDirectories": [
|
||||
"/path/to/another/project",
|
||||
"../shared-library",
|
||||
"~/common-utils"
|
||||
]
|
||||
```
|
||||
|
||||
- **`loadMemoryFromIncludeDirectories`** (boolean):
|
||||
- **Description:** Controls the behavior of the `/memory refresh` command. If set to `true`, `GEMINI.md` files should be loaded from all directories that are added. If set to `false`, `GEMINI.md` should only be loaded from the current directory.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
```json
|
||||
"loadMemoryFromIncludeDirectories": true
|
||||
```
|
||||
|
||||
- **`chatCompression`** (object):
|
||||
- **Description:** Controls the settings for chat history compression, both automatic and
|
||||
when manually invoked through the /compress command.
|
||||
- **Properties:**
|
||||
- **`contextPercentageThreshold`** (number): A value between 0 and 1 that specifies the token threshold for compression as a percentage of the model's total token limit. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit.
|
||||
- **Example:**
|
||||
```json
|
||||
"chatCompression": {
|
||||
"contextPercentageThreshold": 0.6
|
||||
}
|
||||
```
|
||||
|
||||
- **`showLineNumbers`** (boolean):
|
||||
- **Description:** Controls whether line numbers are displayed in code blocks in the CLI output.
|
||||
- **Default:** `true`
|
||||
- **Example:**
|
||||
```json
|
||||
"showLineNumbers": false
|
||||
```
|
||||
|
||||
- **`accessibility`** (object):
|
||||
- **Description:** Configures accessibility features for the CLI.
|
||||
- **Properties:**
|
||||
- **`screenReader`** (boolean): Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. This can also be enabled with the `--screen-reader` command-line flag, which will take precedence over the setting.
|
||||
- **`disableLoadingPhrases`** (boolean): Disables the display of loading phrases during operations.
|
||||
- **Default:** `{"screenReader": false, "disableLoadingPhrases": false}`
|
||||
- **Example:**
|
||||
```json
|
||||
"accessibility": {
|
||||
"screenReader": true,
|
||||
"disableLoadingPhrases": true
|
||||
}
|
||||
```
|
||||
|
||||
### Example `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "GitHub",
|
||||
"sandbox": "docker",
|
||||
"toolDiscoveryCommand": "bin/get_tools",
|
||||
"toolCallCommand": "bin/call_tool",
|
||||
"mcpServers": {
|
||||
"mainServer": {
|
||||
"command": "bin/mcp_server.py"
|
||||
},
|
||||
"anotherServer": {
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js", "--verbose"]
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "http://localhost:4317",
|
||||
"logPrompts": true
|
||||
},
|
||||
"usageStatisticsEnabled": true,
|
||||
"hideTips": false,
|
||||
"hideBanner": false,
|
||||
"maxSessionTurns": 10,
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 100
|
||||
}
|
||||
},
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"],
|
||||
"includeDirectories": ["path/to/dir1", "~/path/to/dir2", "../path/to/dir3"],
|
||||
"loadMemoryFromIncludeDirectories": true
|
||||
}
|
||||
```
|
||||
|
||||
## Shell History
|
||||
|
||||
The CLI keeps a history of shell commands you run. To avoid conflicts between different projects, this history is stored in a project-specific directory within your user's home folder.
|
||||
|
||||
- **Location:** `~/.gemini/tmp/<project_hash>/shell_history`
|
||||
- `<project_hash>` is a unique identifier generated from your project's root path.
|
||||
- The history is stored in a file named `shell_history`.
|
||||
|
||||
## Environment Variables & `.env` Files
|
||||
|
||||
Environment variables are a common way to configure applications, especially for sensitive information like API keys or for settings that might change between environments. For authentication setup, see the [Authentication documentation](./authentication.md) which covers all available authentication methods.
|
||||
|
||||
The CLI automatically loads environment variables from an `.env` file. The loading order is:
|
||||
|
||||
1. `.env` file in the current working directory.
|
||||
2. If not found, it searches upwards in parent directories until it finds an `.env` file or reaches the project root (identified by a `.git` folder) or the home directory.
|
||||
3. If still not found, it looks for `~/.env` (in the user's home directory).
|
||||
|
||||
**Environment Variable Exclusion:** Some environment variables (like `DEBUG` and `DEBUG_MODE`) are automatically excluded from being loaded from project `.env` files to prevent interference with gemini-cli behavior. Variables from `.gemini/.env` files are never excluded. You can customize this behavior using the `excludedProjectEnvVars` setting in your `settings.json` file.
|
||||
|
||||
- **`GEMINI_API_KEY`**:
|
||||
- Your API key for the Gemini API.
|
||||
- One of several available [authentication methods](./authentication.md).
|
||||
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env` file.
|
||||
- **`GEMINI_MODEL`**:
|
||||
- Specifies the default Gemini model to use.
|
||||
- Overrides the hardcoded default
|
||||
- Example: `export GEMINI_MODEL="gemini-2.5-flash"`
|
||||
- **`GOOGLE_API_KEY`**:
|
||||
- Your Google Cloud API key.
|
||||
- Required for using Vertex AI in express mode.
|
||||
- Ensure you have the necessary permissions.
|
||||
- Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"`.
|
||||
- **`GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID.
|
||||
- Required for using Code Assist or Vertex AI.
|
||||
- If using Vertex AI, ensure you have the necessary permissions in this project.
|
||||
- **Cloud Shell Note:** When running in a Cloud Shell environment, this variable defaults to a special project allocated for Cloud Shell users. If you have `GOOGLE_CLOUD_PROJECT` set in your global environment in Cloud Shell, it will be overridden by this default. To use a different project in Cloud Shell, you must define `GOOGLE_CLOUD_PROJECT` in a `.env` file.
|
||||
- Example: `export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
- **`GOOGLE_APPLICATION_CREDENTIALS`** (string):
|
||||
- **Description:** The path to your Google Application Credentials JSON file.
|
||||
- **Example:** `export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
|
||||
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID for Telemetry in Google Cloud
|
||||
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
- **`GOOGLE_CLOUD_LOCATION`**:
|
||||
- Your Google Cloud Project Location (e.g., us-central1).
|
||||
- Required for using Vertex AI in non express mode.
|
||||
- Example: `export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"`.
|
||||
- **`GEMINI_SANDBOX`**:
|
||||
- Alternative to the `sandbox` setting in `settings.json`.
|
||||
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
|
||||
- **`SEATBELT_PROFILE`** (macOS specific):
|
||||
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
|
||||
- `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations.
|
||||
- `strict`: Uses a strict profile that declines operations by default.
|
||||
- `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.gemini/` directory (e.g., `my-project/.gemini/sandbox-macos-custom.sb`).
|
||||
- **`DEBUG` or `DEBUG_MODE`** (often used by underlying libraries or the CLI itself):
|
||||
- Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting.
|
||||
- **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with gemini-cli behavior. Use `.gemini/.env` files if you need to set these for gemini-cli specifically.
|
||||
- **`NO_COLOR`**:
|
||||
- Set to any value to disable all color output in the CLI.
|
||||
- **`CLI_TITLE`**:
|
||||
- Set to a string to customize the title of the CLI.
|
||||
- **`CODE_ASSIST_ENDPOINT`**:
|
||||
- Specifies the endpoint for the code assist server.
|
||||
- This is useful for development and testing.
|
||||
|
||||
## Command-Line Arguments
|
||||
|
||||
Arguments passed directly when running the CLI can override other configurations for that specific session.
|
||||
|
||||
- **`--model <model_name>`** (**`-m <model_name>`**):
|
||||
- Specifies the Gemini model to use for this session.
|
||||
- Example: `npm start -- --model gemini-1.5-pro-latest`
|
||||
- **`--prompt <your_prompt>`** (**`-p <your_prompt>`**):
|
||||
- Used to pass a prompt directly to the command. This invokes Gemini CLI in a non-interactive mode.
|
||||
- **`--prompt-interactive <your_prompt>`** (**`-i <your_prompt>`**):
|
||||
- Starts an interactive session with the provided prompt as the initial input.
|
||||
- The prompt is processed within the interactive session, not before it.
|
||||
- Cannot be used when piping input from stdin.
|
||||
- Example: `gemini -i "explain this code"`
|
||||
- **`--sandbox`** (**`-s`**):
|
||||
- Enables sandbox mode for this session.
|
||||
- **`--sandbox-image`**:
|
||||
- Sets the sandbox image URI.
|
||||
- **`--debug`** (**`-d`**):
|
||||
- Enables debug mode for this session, providing more verbose output.
|
||||
- **`--all-files`** (**`-a`**):
|
||||
- If set, recursively includes all files within the current directory as context for the prompt.
|
||||
- **`--help`** (or **`-h`**):
|
||||
- Displays help information about command-line arguments.
|
||||
- **`--show-memory-usage`**:
|
||||
- Displays the current memory usage.
|
||||
- **`--yolo`**:
|
||||
- Enables YOLO mode, which automatically approves all tool calls.
|
||||
- **`--approval-mode <mode>`**:
|
||||
- Sets the approval mode for tool calls. Available modes:
|
||||
- `default`: Prompt for approval on each tool call (default behavior)
|
||||
- `auto_edit`: Automatically approve edit tools (replace, write_file) while prompting for others
|
||||
- `yolo`: Automatically approve all tool calls (equivalent to `--yolo`)
|
||||
- Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach.
|
||||
- Example: `gemini --approval-mode auto_edit`
|
||||
- **`--allowed-tools <tool1,tool2,...>`**:
|
||||
- A comma-separated list of tool names that will bypass the confirmation dialog.
|
||||
- Example: `gemini --allowed-tools "ShellTool(git status)"`
|
||||
- **`--telemetry`**:
|
||||
- Enables [telemetry](../telemetry.md).
|
||||
- **`--telemetry-target`**:
|
||||
- Sets the telemetry target. See [telemetry](../telemetry.md) for more information.
|
||||
- **`--telemetry-otlp-endpoint`**:
|
||||
- Sets the OTLP endpoint for telemetry. See [telemetry](../telemetry.md) for more information.
|
||||
- **`--telemetry-otlp-protocol`**:
|
||||
- Sets the OTLP protocol for telemetry (`grpc` or `http`). Defaults to `grpc`. See [telemetry](../telemetry.md) for more information.
|
||||
- **`--telemetry-log-prompts`**:
|
||||
- Enables logging of prompts for telemetry. See [telemetry](../telemetry.md) for more information.
|
||||
- **`--checkpointing`**:
|
||||
- Enables [checkpointing](../checkpointing.md).
|
||||
- **`--extensions <extension_name ...>`** (**`-e <extension_name ...>`**):
|
||||
- Specifies a list of extensions to use for the session. If not provided, all available extensions are used.
|
||||
- Use the special term `gemini -e none` to disable all extensions.
|
||||
- Example: `gemini -e my-extension -e my-other-extension`
|
||||
- **`--list-extensions`** (**`-l`**):
|
||||
- Lists all available extensions and exits.
|
||||
- **`--proxy`**:
|
||||
- Sets the proxy for the CLI.
|
||||
- Example: `--proxy http://localhost:7890`.
|
||||
- **`--include-directories <dir1,dir2,...>`**:
|
||||
- Includes additional directories in the workspace for multi-directory support.
|
||||
- Can be specified multiple times or as comma-separated values.
|
||||
- 5 directories can be added at maximum.
|
||||
- Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2`
|
||||
- **`--screen-reader`**:
|
||||
- Enables screen reader mode for accessibility.
|
||||
- **`--version`**:
|
||||
- Displays the version of the CLI.
|
||||
|
||||
## Context Files (Hierarchical Instructional Context)
|
||||
|
||||
While not strictly configuration for the CLI's _behavior_, context files (defaulting to `GEMINI.md` but configurable via the `contextFileName` setting) are crucial for configuring the _instructional context_ (also referred to as "memory") provided to the Gemini model. This powerful feature allows you to give project-specific instructions, coding style guides, or any relevant background information to the AI, making its responses more tailored and accurate to your needs. The CLI includes UI elements, such as an indicator in the footer showing the number of loaded context files, to keep you informed about the active context.
|
||||
|
||||
- **Purpose:** These Markdown files contain instructions, guidelines, or context that you want the Gemini model to be aware of during your interactions. The system is designed to manage this instructional context hierarchically.
|
||||
|
||||
### Example Context File Content (e.g., `GEMINI.md`)
|
||||
|
||||
Here's a conceptual example of what a context file at the root of a TypeScript project might contain:
|
||||
|
||||
```markdown
|
||||
# Project: My Awesome TypeScript Library
|
||||
|
||||
## General Instructions:
|
||||
|
||||
- When generating new TypeScript code, please follow the existing coding style.
|
||||
- Ensure all new functions and classes have JSDoc comments.
|
||||
- Prefer functional programming paradigms where appropriate.
|
||||
- All code should be compatible with TypeScript 5.0 and Node.js 20+.
|
||||
|
||||
## Coding Style:
|
||||
|
||||
- Use 2 spaces for indentation.
|
||||
- Interface names should be prefixed with `I` (e.g., `IUserService`).
|
||||
- Private class members should be prefixed with an underscore (`_`).
|
||||
- Always use strict equality (`===` and `!==`).
|
||||
|
||||
## Specific Component: `src/api/client.ts`
|
||||
|
||||
- This file handles all outbound API requests.
|
||||
- When adding new API call functions, ensure they include robust error handling and logging.
|
||||
- Use the existing `fetchWithRetry` utility for all GET requests.
|
||||
|
||||
## Regarding Dependencies:
|
||||
|
||||
- Avoid introducing new external dependencies unless absolutely necessary.
|
||||
- If a new dependency is required, please state the reason.
|
||||
```
|
||||
|
||||
This example demonstrates how you can provide general project context, specific coding conventions, and even notes about particular files or components. The more relevant and precise your context files are, the better the AI can assist you. Project-specific context files are highly encouraged to establish conventions and context.
|
||||
|
||||
- **Hierarchical Loading and Precedence:** The CLI implements a sophisticated hierarchical memory system by loading context files (e.g., `GEMINI.md`) from several locations. Content from files lower in this list (more specific) typically overrides or supplements content from files higher up (more general). The exact concatenation order and final context can be inspected using the `/memory show` command. The typical loading order is:
|
||||
1. **Global Context File:**
|
||||
- Location: `~/.gemini/<contextFileName>` (e.g., `~/.gemini/GEMINI.md` in your user home directory).
|
||||
- Scope: Provides default instructions for all your projects.
|
||||
2. **Project Root & Ancestors Context Files:**
|
||||
- Location: The CLI searches for the configured context file in the current working directory and then in each parent directory up to either the project root (identified by a `.git` folder) or your home directory.
|
||||
- Scope: Provides context relevant to the entire project or a significant portion of it.
|
||||
3. **Sub-directory Context Files (Contextual/Local):**
|
||||
- Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with a `memoryDiscoveryMaxDirs` field in your `settings.json` file.
|
||||
- Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project.
|
||||
- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt to the Gemini model. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
|
||||
- **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory Import Processor documentation](../core/memport.md).
|
||||
- **Commands for Memory Management:**
|
||||
- Use `/memory refresh` to force a re-scan and reload of all context files from all configured locations. This updates the AI's instructional context.
|
||||
- Use `/memory show` to display the combined instructional context currently loaded, allowing you to verify the hierarchy and content being used by the AI.
|
||||
- See the [Commands documentation](./commands.md#memory) for full details on the `/memory` command and its sub-commands (`show` and `refresh`).
|
||||
|
||||
By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor the Gemini CLI's responses to your specific needs and projects.
|
||||
|
||||
## Sandboxing
|
||||
|
||||
The Gemini CLI can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system.
|
||||
|
||||
Sandboxing is disabled by default, but you can enable it in a few ways:
|
||||
|
||||
- Using `--sandbox` or `-s` flag.
|
||||
- Setting `GEMINI_SANDBOX` environment variable.
|
||||
- Sandbox is enabled when using `--yolo` or `--approval-mode=yolo` by default.
|
||||
|
||||
By default, it uses a pre-built `gemini-cli-sandbox` Docker image.
|
||||
|
||||
For project-specific sandboxing needs, you can create a custom Dockerfile at `.gemini/sandbox.Dockerfile` in your project's root directory. This Dockerfile can be based on the base sandbox image:
|
||||
|
||||
```dockerfile
|
||||
FROM gemini-cli-sandbox
|
||||
|
||||
# Add your custom dependencies or configurations here
|
||||
# For example:
|
||||
# RUN apt-get update && apt-get install -y some-package
|
||||
# COPY ./my-config /app/my-config
|
||||
```
|
||||
|
||||
When `.gemini/sandbox.Dockerfile` exists, you can use `BUILD_SANDBOX` environment variable when running Gemini CLI to automatically build the custom sandbox image:
|
||||
|
||||
```bash
|
||||
BUILD_SANDBOX=1 gemini -s
|
||||
```
|
||||
|
||||
## Usage Statistics
|
||||
|
||||
To help us improve the Gemini CLI, we collect anonymized usage statistics. This data helps us understand how the CLI is used, identify common issues, and prioritize new features.
|
||||
|
||||
**What we collect:**
|
||||
|
||||
- **Tool Calls:** We log the names of the tools that are called, whether they succeed or fail, and how long they take to execute. We do not collect the arguments passed to the tools or any data returned by them.
|
||||
- **API Requests:** We log the Gemini model used for each request, the duration of the request, and whether it was successful. We do not collect the content of the prompts or responses.
|
||||
- **Session Information:** We collect information about the configuration of the CLI, such as the enabled tools and the approval mode.
|
||||
|
||||
**What we DON'T collect:**
|
||||
|
||||
- **Personally Identifiable Information (PII):** We do not collect any personal information, such as your name, email address, or API keys.
|
||||
- **Prompt and Response Content:** We do not log the content of your prompts or the responses from the Gemini model.
|
||||
- **File Content:** We do not log the content of any files that are read or written by the CLI.
|
||||
|
||||
**How to opt out:**
|
||||
|
||||
You can opt out of usage statistics collection at any time by setting the `usageStatisticsEnabled` property to `false` in your `settings.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"usageStatisticsEnabled": false
|
||||
}
|
||||
```
|
||||
+312
-290
@@ -1,5 +1,14 @@
|
||||
# Gemini CLI Configuration
|
||||
|
||||
**Note on New Configuration Format**
|
||||
|
||||
The format of the `settings.json` file has been updated to a new, more organized structure.
|
||||
|
||||
- The new format will be supported in the stable release starting **[09/10/25]**.
|
||||
- Automatic migration from the old format to the new format will begin on **[09/17/25]**.
|
||||
|
||||
For details on the previous format, please see the [v1 Configuration documentation](./configuration-v1.md).
|
||||
|
||||
Gemini CLI offers several ways to configure its behavior, including environment variables, command-line arguments, and settings files. This document outlines the different configuration methods and available settings.
|
||||
|
||||
## Configuration layers
|
||||
@@ -41,316 +50,288 @@ In addition to a project settings file, a project's `.gemini` directory can cont
|
||||
|
||||
- [Custom sandbox profiles](#sandboxing) (e.g., `.gemini/sandbox-macos-custom.sb`, `.gemini/sandbox.Dockerfile`).
|
||||
|
||||
### Available settings in `settings.json`:
|
||||
### Available settings in `settings.json`
|
||||
|
||||
- **`contextFileName`** (string or array of strings):
|
||||
- **Description:** Specifies the filename for context files (e.g., `GEMINI.md`, `AGENTS.md`). Can be a single filename or a list of accepted filenames.
|
||||
- **Default:** `GEMINI.md`
|
||||
- **Example:** `"contextFileName": "AGENTS.md"`
|
||||
Settings are organized into categories. All settings should be placed within their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`bugCommand`** (object):
|
||||
- **Description:** Overrides the default URL for the `/bug` command.
|
||||
- **Default:** `"urlTemplate": "https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}"`
|
||||
- **Properties:**
|
||||
- **`urlTemplate`** (string): A URL that can contain `{title}` and `{info}` placeholders.
|
||||
- **Example:**
|
||||
```json
|
||||
"bugCommand": {
|
||||
"urlTemplate": "https://bug.example.com/new?title={title}&info={info}"
|
||||
}
|
||||
```
|
||||
#### `general`
|
||||
|
||||
- **`fileFiltering`** (object):
|
||||
- **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools.
|
||||
- **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true`
|
||||
- **Properties:**
|
||||
- **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations.
|
||||
- **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt.
|
||||
- **`disableFuzzySearch`** (boolean): When `true`, disables the fuzzy search capabilities when searching for files, which can improve performance on projects with a large number of files.
|
||||
- **Example:**
|
||||
```json
|
||||
"fileFiltering": {
|
||||
"respectGitIgnore": true,
|
||||
"enableRecursiveFileSearch": false,
|
||||
"disableFuzzySearch": true
|
||||
}
|
||||
```
|
||||
|
||||
### Troubleshooting File Search Performance
|
||||
|
||||
If you are experiencing performance issues with file searching (e.g., with `@` completions), especially in projects with a very large number of files, here are a few things you can try in order of recommendation:
|
||||
|
||||
1. **Use `.geminiignore`:** Create a `.geminiignore` file in your project root to exclude directories that contain a large number of files that you don't need to reference (e.g., build artifacts, logs, `node_modules`). Reducing the total number of files crawled is the most effective way to improve performance.
|
||||
|
||||
2. **Disable Fuzzy Search:** If ignoring files is not enough, you can disable fuzzy search by setting `disableFuzzySearch` to `true` in your `settings.json` file. This will use a simpler, non-fuzzy matching algorithm, which can be faster.
|
||||
|
||||
3. **Disable Recursive File Search:** As a last resort, you can disable recursive file search entirely by setting `enableRecursiveFileSearch` to `false`. This will be the fastest option as it avoids a recursive crawl of your project. However, it means you will need to type the full path to files when using `@` completions.
|
||||
|
||||
- **`coreTools`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to be executed.
|
||||
- **Default:** All tools available for use by the Gemini model.
|
||||
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`.
|
||||
|
||||
- **`allowedTools`** (array of strings):
|
||||
- **`general.preferredEditor`** (string):
|
||||
- **Description:** The preferred editor to open files in.
|
||||
- **Default:** `undefined`
|
||||
- **Description:** A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. The match semantics are the same as `coreTools`.
|
||||
- **Example:** `"allowedTools": ["ShellTool(git status)"]`.
|
||||
|
||||
- **`excludeTools`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command.
|
||||
- **Default**: No tools excluded.
|
||||
- **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
|
||||
- **Security Note:** Command-specific restrictions in
|
||||
`excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands
|
||||
that can be executed.
|
||||
|
||||
- **`allowMCPServers`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of MCP server names that should be made available to the model. This can be used to restrict the set of MCP servers to connect to. Note that this will be ignored if `--allowed-mcp-server-names` is set.
|
||||
- **Default:** All MCP servers are available for use by the Gemini model.
|
||||
- **Example:** `"allowMCPServers": ["myPythonServer"]`.
|
||||
- **Security Note:** This uses simple string matching on MCP server names, which can be modified. If you're a system administrator looking to prevent users from bypassing this, consider configuring the `mcpServers` at the system settings level such that the user will not be able to configure any MCP servers of their own. This should not be used as an airtight security mechanism.
|
||||
|
||||
- **`excludeMCPServers`** (array of strings):
|
||||
- **Description:** Allows you to specify a list of MCP server names that should be excluded from the model. A server listed in both `excludeMCPServers` and `allowMCPServers` is excluded. Note that this will be ignored if `--allowed-mcp-server-names` is set.
|
||||
- **Default**: No MCP servers excluded.
|
||||
- **Example:** `"excludeMCPServers": ["myNodeServer"]`.
|
||||
- **Security Note:** This uses simple string matching on MCP server names, which can be modified. If you're a system administrator looking to prevent users from bypassing this, consider configuring the `mcpServers` at the system settings level such that the user will not be able to configure any MCP servers of their own. This should not be used as an airtight security mechanism.
|
||||
|
||||
- **`autoAccept`** (boolean):
|
||||
- **Description:** Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe.
|
||||
- **`general.vimMode`** (boolean):
|
||||
- **Description:** Enable Vim keybindings.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"autoAccept": true`
|
||||
|
||||
- **`theme`** (string):
|
||||
- **Description:** Sets the visual [theme](./themes.md) for Gemini CLI.
|
||||
- **Default:** `"Default"`
|
||||
- **Example:** `"theme": "GitHub"`
|
||||
|
||||
- **`vimMode`** (boolean):
|
||||
- **Description:** Enables or disables vim mode for input editing. When enabled, the input area supports vim-style navigation and editing commands with NORMAL and INSERT modes. The vim mode status is displayed in the footer and persists between sessions.
|
||||
- **`general.disableAutoUpdate`** (boolean):
|
||||
- **Description:** Disable automatic updates.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"vimMode": true`
|
||||
|
||||
- **`sandbox`** (boolean or string):
|
||||
- **Description:** Controls whether and how to use sandboxing for tool execution. If set to `true`, Gemini CLI uses a pre-built `gemini-cli-sandbox` Docker image. For more information, see [Sandboxing](#sandboxing).
|
||||
- **`general.disableUpdateNag`** (boolean):
|
||||
- **Description:** Disable update notification prompts.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"sandbox": "docker"`
|
||||
|
||||
- **`toolDiscoveryCommand`** (string):
|
||||
- **Description:** Defines a custom shell command for discovering tools from your project. The shell command must return on `stdout` a JSON array of [function declarations](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations). Tool wrappers are optional.
|
||||
- **Default:** Empty
|
||||
- **Example:** `"toolDiscoveryCommand": "bin/get_tools"`
|
||||
- **`general.checkpointing.enabled`** (boolean):
|
||||
- **Description:** Enable session checkpointing for recovery.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`toolCallCommand`** (string):
|
||||
- **Description:** Defines a custom shell command for calling a specific tool that was discovered using `toolDiscoveryCommand`. The shell command must meet the following criteria:
|
||||
#### `output`
|
||||
|
||||
- **`output.format`** (string):
|
||||
- **Description:** The format of the CLI output.
|
||||
- **Default:** `"text"`
|
||||
- **Values:** `"text"`, `"json"`
|
||||
|
||||
#### `ui`
|
||||
|
||||
- **`ui.theme`** (string):
|
||||
- **Description:** The color theme for the UI. See [Themes](./themes.md) for available options.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`ui.customThemes`** (object):
|
||||
- **Description:** Custom theme definitions.
|
||||
- **Default:** `{}`
|
||||
|
||||
- **`ui.hideWindowTitle`** (boolean):
|
||||
- **Description:** Hide the window title bar.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.hideTips`** (boolean):
|
||||
- **Description:** Hide helpful tips in the UI.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.hideBanner`** (boolean):
|
||||
- **Description:** Hide the application banner.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.hideFooter`** (boolean):
|
||||
- **Description:** Hide the footer from the UI.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.showMemoryUsage`** (boolean):
|
||||
- **Description:** Display memory usage information in the UI.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.showLineNumbers`** (boolean):
|
||||
- **Description:** Show line numbers in the chat.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.showCitations`** (boolean):
|
||||
- **Description:** Show citations for generated text in the chat.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.accessibility.disableLoadingPhrases`** (boolean):
|
||||
- **Description:** Disable loading phrases for accessibility.
|
||||
- **Default:** `false`
|
||||
|
||||
#### `ide`
|
||||
|
||||
- **`ide.enabled`** (boolean):
|
||||
- **Description:** Enable IDE integration mode.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ide.hasSeenNudge`** (boolean):
|
||||
- **Description:** Whether the user has seen the IDE integration nudge.
|
||||
- **Default:** `false`
|
||||
|
||||
#### `privacy`
|
||||
|
||||
- **`privacy.usageStatisticsEnabled`** (boolean):
|
||||
- **Description:** Enable collection of usage statistics.
|
||||
- **Default:** `true`
|
||||
|
||||
#### `model`
|
||||
|
||||
- **`model.name`** (string):
|
||||
- **Description:** The Gemini model to use for conversations.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`model.maxSessionTurns`** (number):
|
||||
- **Description:** Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.
|
||||
- **Default:** `-1`
|
||||
|
||||
- **`model.summarizeToolOutput`** (object):
|
||||
- **Description:** Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting. Note: Currently only the `run_shell_command` tool is supported. For example `{"run_shell_command": {"tokenBudget": 2000}}`
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`model.chatCompression.contextPercentageThreshold`** (number):
|
||||
- **Description:** Sets the threshold for chat history compression as a percentage of the model's total token limit. This is a value between 0 and 1 that applies to both automatic compression and the manual `/compress` command. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit.
|
||||
- **Default:** `0.7`
|
||||
|
||||
- **`model.skipNextSpeakerCheck`** (boolean):
|
||||
- **Description:** Skip the next speaker check.
|
||||
- **Default:** `false`
|
||||
|
||||
#### `context`
|
||||
|
||||
- **`context.fileName`** (string or array of strings):
|
||||
- **Description:** The name of the context file(s).
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`context.importFormat`** (string):
|
||||
- **Description:** The format to use when importing memory.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`context.discoveryMaxDirs`** (number):
|
||||
- **Description:** Maximum number of directories to search for memory.
|
||||
- **Default:** `200`
|
||||
|
||||
- **`context.includeDirectories`** (array):
|
||||
- **Description:** Additional directories to include in the workspace context. Missing directories will be skipped with a warning.
|
||||
- **Default:** `[]`
|
||||
|
||||
- **`context.loadFromIncludeDirectories`** (boolean):
|
||||
- **Description:** Controls the behavior of the `/memory refresh` command. If set to `true`, `GEMINI.md` files should be loaded from all directories that are added. If set to `false`, `GEMINI.md` should only be loaded from the current directory.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`context.fileFiltering.respectGitIgnore`** (boolean):
|
||||
- **Description:** Respect .gitignore files when searching.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`context.fileFiltering.respectGeminiIgnore`** (boolean):
|
||||
- **Description:** Respect .geminiignore files when searching.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`context.fileFiltering.enableRecursiveFileSearch`** (boolean):
|
||||
- **Description:** Whether to enable searching recursively for filenames under the current tree when completing `@` prefixes in the prompt.
|
||||
- **Default:** `true`
|
||||
|
||||
#### `tools`
|
||||
|
||||
- **`tools.sandbox`** (boolean or string):
|
||||
- **Description:** Sandbox execution environment (can be a boolean or a path string).
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`tools.shell.enableInteractiveShell`** (boolean):
|
||||
|
||||
Use `node-pty` for an interactive shell experience. Fallback to `child_process` still applies. Defaults to `false`.
|
||||
|
||||
- **`tools.core`** (array of strings):
|
||||
- **Description:** This can be used to restrict the set of built-in tools [with an allowlist](./enterprise.md#restricting-tool-access). See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. The match semantics are the same as `tools.allowed`.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`tools.exclude`** (array of strings):
|
||||
- **Description:** Tool names to exclude from discovery.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`tools.allowed`** (array of strings):
|
||||
- **Description:** A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. For example, `["run_shell_command(git)", "run_shell_command(npm test)"]` will skip the confirmation dialog to run any `git` and `npm test` commands. See [Shell Tool command restrictions](../tools/shell.md#command-restrictions) for details on prefix matching, command chaining, etc.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`tools.discoveryCommand`** (string):
|
||||
- **Description:** Command to run for tool discovery.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`tools.callCommand`** (string):
|
||||
- **Description:** Defines a custom shell command for calling a specific tool that was discovered using `tools.discoveryCommand`. The shell command must meet the following criteria:
|
||||
- It must take function `name` (exactly as in [function declaration](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations)) as first command line argument.
|
||||
- It must read function arguments as JSON on `stdin`, analogous to [`functionCall.args`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functioncall).
|
||||
- It must return function output as JSON on `stdout`, analogous to [`functionResponse.response.content`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functionresponse).
|
||||
- **Default:** Empty
|
||||
- **Example:** `"toolCallCommand": "bin/call_tool"`
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`mcpServers`** (object):
|
||||
- **Description:** Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Gemini CLI attempts to connect to each configured MCP server to discover available tools. If multiple MCP servers expose a tool with the same name, the tool names will be prefixed with the server alias you defined in the configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note that the system might strip certain schema properties from MCP tool definitions for compatibility. At least one of `command`, `url`, or `httpUrl` must be provided. If multiple are specified, the order of precedence is `httpUrl`, then `url`, then `command`.
|
||||
- **Default:** Empty
|
||||
- **Properties:**
|
||||
- **`<SERVER_NAME>`** (object): The server parameters for the named server.
|
||||
- `command` (string, optional): The command to execute to start the MCP server via standard I/O.
|
||||
- `args` (array of strings, optional): Arguments to pass to the command.
|
||||
- `env` (object, optional): Environment variables to set for the server process.
|
||||
- `cwd` (string, optional): The working directory in which to start the server.
|
||||
- `url` (string, optional): The URL of an MCP server that uses Server-Sent Events (SSE) for communication.
|
||||
- `httpUrl` (string, optional): The URL of an MCP server that uses streamable HTTP for communication.
|
||||
- `headers` (object, optional): A map of HTTP headers to send with requests to `url` or `httpUrl`.
|
||||
- `timeout` (number, optional): Timeout in milliseconds for requests to this MCP server.
|
||||
- `trust` (boolean, optional): Trust this server and bypass all tool call confirmations.
|
||||
- `description` (string, optional): A brief description of the server, which may be used for display purposes.
|
||||
- `includeTools` (array of strings, optional): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (whitelist behavior). If not specified, all tools from the server are enabled by default.
|
||||
- `excludeTools` (array of strings, optional): List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded.
|
||||
- **Example:**
|
||||
```json
|
||||
"mcpServers": {
|
||||
"myPythonServer": {
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--port", "8080"],
|
||||
"cwd": "./mcp_tools/python",
|
||||
"timeout": 5000,
|
||||
"includeTools": ["safe_tool", "file_reader"],
|
||||
},
|
||||
"myNodeServer": {
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js"],
|
||||
"cwd": "./mcp_tools/node",
|
||||
"excludeTools": ["dangerous_tool", "file_deleter"]
|
||||
},
|
||||
"myDockerServer": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "-e", "API_KEY", "ghcr.io/foo/bar"],
|
||||
"env": {
|
||||
"API_KEY": "$MY_API_TOKEN"
|
||||
}
|
||||
},
|
||||
"mySseServer": {
|
||||
"url": "http://localhost:8081/events",
|
||||
"headers": {
|
||||
"Authorization": "Bearer $MY_SSE_TOKEN"
|
||||
},
|
||||
"description": "An example SSE-based MCP server."
|
||||
},
|
||||
"myStreamableHttpServer": {
|
||||
"httpUrl": "http://localhost:8082/stream",
|
||||
"headers": {
|
||||
"X-API-Key": "$MY_HTTP_API_KEY"
|
||||
},
|
||||
"description": "An example Streamable HTTP-based MCP server."
|
||||
}
|
||||
}
|
||||
```
|
||||
#### `mcp`
|
||||
|
||||
- **`checkpointing`** (object):
|
||||
- **Description:** Configures the checkpointing feature, which allows you to save and restore conversation and file states. See the [Checkpointing documentation](../checkpointing.md) for more details.
|
||||
- **Default:** `{"enabled": false}`
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): When `true`, the `/restore` command is available.
|
||||
- **`mcp.serverCommand`** (string):
|
||||
- **Description:** Command to start an MCP server.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`preferredEditor`** (string):
|
||||
- **Description:** Specifies the preferred editor to use for viewing diffs.
|
||||
- **Default:** `vscode`
|
||||
- **Example:** `"preferredEditor": "vscode"`
|
||||
- **`mcp.allowed`** (array of strings):
|
||||
- **Description:** An allowlist of MCP servers to allow.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`telemetry`** (object)
|
||||
- **Description:** Configures logging and metrics collection for Gemini CLI. For more information, see [Telemetry](../telemetry.md).
|
||||
- **Default:** `{"enabled": false, "target": "local", "otlpEndpoint": "http://localhost:4317", "logPrompts": true}`
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): Whether or not telemetry is enabled.
|
||||
- **`target`** (string): The destination for collected telemetry. Supported values are `local` and `gcp`.
|
||||
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
|
||||
- **`logPrompts`** (boolean): Whether or not to include the content of user prompts in the logs.
|
||||
- **Example:**
|
||||
```json
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "http://localhost:16686",
|
||||
"logPrompts": false
|
||||
}
|
||||
```
|
||||
- **`usageStatisticsEnabled`** (boolean):
|
||||
- **Description:** Enables or disables the collection of usage statistics. See [Usage Statistics](#usage-statistics) for more information.
|
||||
- **Default:** `true`
|
||||
- **Example:**
|
||||
```json
|
||||
"usageStatisticsEnabled": false
|
||||
```
|
||||
- **`mcp.excluded`** (array of strings):
|
||||
- **Description:** A denylist of MCP servers to exclude.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`hideTips`** (boolean):
|
||||
- **Description:** Enables or disables helpful tips in the CLI interface.
|
||||
#### `security`
|
||||
|
||||
- **`security.folderTrust.enabled`** (boolean):
|
||||
- **Description:** Setting to track whether Folder trust is enabled.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
|
||||
```json
|
||||
"hideTips": true
|
||||
```
|
||||
- **`security.auth.selectedType`** (string):
|
||||
- **Description:** The currently selected authentication type.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`hideBanner`** (boolean):
|
||||
- **Description:** Enables or disables the startup banner (ASCII art logo) in the CLI interface.
|
||||
- **`security.auth.enforcedType`** (string):
|
||||
- **Description:** The required auth type (useful for enterprises).
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`security.auth.useExternal`** (boolean):
|
||||
- **Description:** Whether to use an external authentication flow.
|
||||
- **Default:** `undefined`
|
||||
|
||||
#### `advanced`
|
||||
|
||||
- **`advanced.autoConfigureMemory`** (boolean):
|
||||
- **Description:** Automatically configure Node.js memory limits.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
|
||||
```json
|
||||
"hideBanner": true
|
||||
```
|
||||
- **`advanced.dnsResolutionOrder`** (string):
|
||||
- **Description:** The DNS resolution order.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`maxSessionTurns`** (number):
|
||||
- **Description:** Sets the maximum number of turns for a session. If the session exceeds this limit, the CLI will stop processing and start a new chat.
|
||||
- **Default:** `-1` (unlimited)
|
||||
- **Example:**
|
||||
```json
|
||||
"maxSessionTurns": 10
|
||||
```
|
||||
- **`advanced.excludedEnvVars`** (array of strings):
|
||||
- **Description:** Environment variables to exclude from project context.
|
||||
- **Default:** `["DEBUG","DEBUG_MODE"]`
|
||||
|
||||
- **`summarizeToolOutput`** (object):
|
||||
- **Description:** Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting.
|
||||
- Note: Currently only the `run_shell_command` tool is supported.
|
||||
- **Default:** `{}` (Disabled by default)
|
||||
- **Example:**
|
||||
```json
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 2000
|
||||
}
|
||||
}
|
||||
```
|
||||
- **`advanced.bugCommand`** (object):
|
||||
- **Description:** Configuration for the bug report command.
|
||||
- **Default:** `undefined`
|
||||
|
||||
- **`excludedProjectEnvVars`** (array of strings):
|
||||
- **Description:** Specifies environment variables that should be excluded from being loaded from project `.env` files. This prevents project-specific environment variables (like `DEBUG=true`) from interfering with gemini-cli behavior. Variables from `.gemini/.env` files are never excluded.
|
||||
- **Default:** `["DEBUG", "DEBUG_MODE"]`
|
||||
- **Example:**
|
||||
```json
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
|
||||
```
|
||||
#### `mcpServers`
|
||||
|
||||
- **`includeDirectories`** (array of strings):
|
||||
- **Description:** Specifies an array of additional absolute or relative paths to include in the workspace context. Missing directories will be skipped with a warning by default. Paths can use `~` to refer to the user's home directory. This setting can be combined with the `--include-directories` command-line flag.
|
||||
- **Default:** `[]`
|
||||
- **Example:**
|
||||
```json
|
||||
"includeDirectories": [
|
||||
"/path/to/another/project",
|
||||
"../shared-library",
|
||||
"~/common-utils"
|
||||
]
|
||||
```
|
||||
Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Gemini CLI attempts to connect to each configured MCP server to discover available tools. If multiple MCP servers expose a tool with the same name, the tool names will be prefixed with the server alias you defined in the configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note that the system might strip certain schema properties from MCP tool definitions for compatibility. At least one of `command`, `url`, or `httpUrl` must be provided. If multiple are specified, the order of precedence is `httpUrl`, then `url`, then `command`.
|
||||
|
||||
- **`loadMemoryFromIncludeDirectories`** (boolean):
|
||||
- **Description:** Controls the behavior of the `/memory refresh` command. If set to `true`, `GEMINI.md` files should be loaded from all directories that are added. If set to `false`, `GEMINI.md` should only be loaded from the current directory.
|
||||
- **Default:** `false`
|
||||
- **Example:**
|
||||
```json
|
||||
"loadMemoryFromIncludeDirectories": true
|
||||
```
|
||||
- **`mcpServers.<SERVER_NAME>`** (object): The server parameters for the named server.
|
||||
- `command` (string, optional): The command to execute to start the MCP server via standard I/O.
|
||||
- `args` (array of strings, optional): Arguments to pass to the command.
|
||||
- `env` (object, optional): Environment variables to set for the server process.
|
||||
- `cwd` (string, optional): The working directory in which to start the server.
|
||||
- `url` (string, optional): The URL of an MCP server that uses Server-Sent Events (SSE) for communication.
|
||||
- `httpUrl` (string, optional): The URL of an MCP server that uses streamable HTTP for communication.
|
||||
- `headers` (object, optional): A map of HTTP headers to send with requests to `url` or `httpUrl`.
|
||||
- `timeout` (number, optional): Timeout in milliseconds for requests to this MCP server.
|
||||
- `trust` (boolean, optional): Trust this server and bypass all tool call confirmations.
|
||||
- `description` (string, optional): A brief description of the server, which may be used for display purposes.
|
||||
- `includeTools` (array of strings, optional): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default.
|
||||
- `excludeTools` (array of strings, optional): List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded.
|
||||
|
||||
- **`chatCompression`** (object):
|
||||
- **Description:** Controls the settings for chat history compression, both automatic and
|
||||
when manually invoked through the /compress command.
|
||||
- **Properties:**
|
||||
- **`contextPercentageThreshold`** (number): A value between 0 and 1 that specifies the token threshold for compression as a percentage of the model's total token limit. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit.
|
||||
- **Example:**
|
||||
```json
|
||||
"chatCompression": {
|
||||
"contextPercentageThreshold": 0.6
|
||||
}
|
||||
```
|
||||
#### `telemetry`
|
||||
|
||||
- **`showLineNumbers`** (boolean):
|
||||
- **Description:** Controls whether line numbers are displayed in code blocks in the CLI output.
|
||||
- **Default:** `true`
|
||||
- **Example:**
|
||||
```json
|
||||
"showLineNumbers": false
|
||||
```
|
||||
Configures logging and metrics collection for Gemini CLI. For more information, see [Telemetry](../telemetry.md).
|
||||
|
||||
- **`accessibility`** (object):
|
||||
- **Description:** Configures accessibility features for the CLI.
|
||||
- **Properties:**
|
||||
- **`screenReader`** (boolean): Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. This can also be enabled with the `--screen-reader` command-line flag, which will take precedence over the setting.
|
||||
- **`disableLoadingPhrases`** (boolean): Disables the display of loading phrases during operations.
|
||||
- **Default:** `{"screenReader": false, "disableLoadingPhrases": false}`
|
||||
- **Example:**
|
||||
```json
|
||||
"accessibility": {
|
||||
"screenReader": true,
|
||||
"disableLoadingPhrases": true
|
||||
}
|
||||
```
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): Whether or not telemetry is enabled.
|
||||
- **`target`** (string): The destination for collected telemetry. Supported values are `local` and `gcp`.
|
||||
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
|
||||
- **`otlpProtocol`** (string): The protocol for the OTLP Exporter (`grpc` or `http`).
|
||||
- **`logPrompts`** (boolean): Whether or not to include the content of user prompts in the logs.
|
||||
- **`outfile`** (string): The file to write telemetry to when `target` is `local`.
|
||||
- **`useCollector`** (boolean): Whether to use an external OTLP collector.
|
||||
|
||||
### Example `settings.json`:
|
||||
### Example `settings.json`
|
||||
|
||||
Here is an example of a `settings.json` file with the nested structure, new as of v0.3.0:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "GitHub",
|
||||
"sandbox": "docker",
|
||||
"toolDiscoveryCommand": "bin/get_tools",
|
||||
"toolCallCommand": "bin/call_tool",
|
||||
"general": {
|
||||
"vimMode": true,
|
||||
"preferredEditor": "code"
|
||||
},
|
||||
"ui": {
|
||||
"theme": "GitHub",
|
||||
"hideBanner": true,
|
||||
"hideTips": false
|
||||
},
|
||||
"tools": {
|
||||
"sandbox": "docker",
|
||||
"discoveryCommand": "bin/get_tools",
|
||||
"callCommand": "bin/call_tool",
|
||||
"exclude": ["write_file"]
|
||||
},
|
||||
"mcpServers": {
|
||||
"mainServer": {
|
||||
"command": "bin/mcp_server.py"
|
||||
@@ -366,18 +347,29 @@ If you are experiencing performance issues with file searching (e.g., with `@` c
|
||||
"otlpEndpoint": "http://localhost:4317",
|
||||
"logPrompts": true
|
||||
},
|
||||
"usageStatisticsEnabled": true,
|
||||
"hideTips": false,
|
||||
"hideBanner": false,
|
||||
"maxSessionTurns": 10,
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 100
|
||||
"privacy": {
|
||||
"usageStatisticsEnabled": true
|
||||
},
|
||||
"model": {
|
||||
"name": "gemini-1.5-pro-latest",
|
||||
"maxSessionTurns": 10,
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"],
|
||||
"includeDirectories": ["path/to/dir1", "~/path/to/dir2", "../path/to/dir3"],
|
||||
"loadMemoryFromIncludeDirectories": true
|
||||
"context": {
|
||||
"fileName": ["CONTEXT.md", "GEMINI.md"],
|
||||
"includeDirectories": ["path/to/dir1", "~/path/to/dir2", "../path/to/dir3"],
|
||||
"loadFromIncludeDirectories": true,
|
||||
"fileFiltering": {
|
||||
"respectGitIgnore": false
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"excludedEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -399,7 +391,7 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
|
||||
2. If not found, it searches upwards in parent directories until it finds an `.env` file or reaches the project root (identified by a `.git` folder) or the home directory.
|
||||
3. If still not found, it looks for `~/.env` (in the user's home directory).
|
||||
|
||||
**Environment Variable Exclusion:** Some environment variables (like `DEBUG` and `DEBUG_MODE`) are automatically excluded from being loaded from project `.env` files to prevent interference with gemini-cli behavior. Variables from `.gemini/.env` files are never excluded. You can customize this behavior using the `excludedProjectEnvVars` setting in your `settings.json` file.
|
||||
**Environment Variable Exclusion:** Some environment variables (like `DEBUG` and `DEBUG_MODE`) are automatically excluded from being loaded from project `.env` files to prevent interference with gemini-cli behavior. Variables from `.gemini/.env` files are never excluded. You can customize this behavior using the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
|
||||
- **`GEMINI_API_KEY`**:
|
||||
- Your API key for the Gemini API.
|
||||
@@ -426,6 +418,27 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
|
||||
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID for Telemetry in Google Cloud
|
||||
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
- **`GEMINI_TELEMETRY_ENABLED`**:
|
||||
- Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it.
|
||||
- Overrides the `telemetry.enabled` setting.
|
||||
- **`GEMINI_TELEMETRY_TARGET`**:
|
||||
- Sets the telemetry target (`local` or `gcp`).
|
||||
- Overrides the `telemetry.target` setting.
|
||||
- **`GEMINI_TELEMETRY_OTLP_ENDPOINT`**:
|
||||
- Sets the OTLP endpoint for telemetry.
|
||||
- Overrides the `telemetry.otlpEndpoint` setting.
|
||||
- **`GEMINI_TELEMETRY_OTLP_PROTOCOL`**:
|
||||
- Sets the OTLP protocol (`grpc` or `http`).
|
||||
- Overrides the `telemetry.otlpProtocol` setting.
|
||||
- **`GEMINI_TELEMETRY_LOG_PROMPTS`**:
|
||||
- Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it.
|
||||
- Overrides the `telemetry.logPrompts` setting.
|
||||
- **`GEMINI_TELEMETRY_OUTFILE`**:
|
||||
- Sets the file path to write telemetry to when the target is `local`.
|
||||
- Overrides the `telemetry.outfile` setting.
|
||||
- **`GEMINI_TELEMETRY_USE_COLLECTOR`**:
|
||||
- Set to `true` or `1` to enable or disable using an external OTLP collector. Any other value is treated as disabling it.
|
||||
- Overrides the `telemetry.useCollector` setting.
|
||||
- **`GOOGLE_CLOUD_LOCATION`**:
|
||||
- Your Google Cloud Project Location (e.g., us-central1).
|
||||
- Required for using Vertex AI in non express mode.
|
||||
@@ -458,11 +471,18 @@ Arguments passed directly when running the CLI can override other configurations
|
||||
- Example: `npm start -- --model gemini-1.5-pro-latest`
|
||||
- **`--prompt <your_prompt>`** (**`-p <your_prompt>`**):
|
||||
- Used to pass a prompt directly to the command. This invokes Gemini CLI in a non-interactive mode.
|
||||
- For scripting examples, use the `--output-format json` flag to get structured output.
|
||||
- **`--prompt-interactive <your_prompt>`** (**`-i <your_prompt>`**):
|
||||
- Starts an interactive session with the provided prompt as the initial input.
|
||||
- The prompt is processed within the interactive session, not before it.
|
||||
- Cannot be used when piping input from stdin.
|
||||
- Example: `gemini -i "explain this code"`
|
||||
- **`--output-format <format>`**:
|
||||
- **Description:** Specifies the format of the CLI output for non-interactive mode.
|
||||
- **Values:**
|
||||
- `text`: (Default) The standard human-readable output.
|
||||
- `json`: A machine-readable JSON output.
|
||||
- **Note:** For structured output and scripting, use the `--output-format json` flag.
|
||||
- **`--sandbox`** (**`-s`**):
|
||||
- Enables sandbox mode for this session.
|
||||
- **`--sandbox-image`**:
|
||||
@@ -514,13 +534,13 @@ Arguments passed directly when running the CLI can override other configurations
|
||||
- 5 directories can be added at maximum.
|
||||
- Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2`
|
||||
- **`--screen-reader`**:
|
||||
- Enables screen reader mode for accessibility.
|
||||
- Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers.
|
||||
- **`--version`**:
|
||||
- Displays the version of the CLI.
|
||||
|
||||
## Context Files (Hierarchical Instructional Context)
|
||||
|
||||
While not strictly configuration for the CLI's _behavior_, context files (defaulting to `GEMINI.md` but configurable via the `contextFileName` setting) are crucial for configuring the _instructional context_ (also referred to as "memory") provided to the Gemini model. This powerful feature allows you to give project-specific instructions, coding style guides, or any relevant background information to the AI, making its responses more tailored and accurate to your needs. The CLI includes UI elements, such as an indicator in the footer showing the number of loaded context files, to keep you informed about the active context.
|
||||
While not strictly configuration for the CLI's _behavior_, context files (defaulting to `GEMINI.md` but configurable via the `context.fileName` setting) are crucial for configuring the _instructional context_ (also referred to as "memory") provided to the Gemini model. This powerful feature allows you to give project-specific instructions, coding style guides, or any relevant background information to the AI, making its responses more tailored and accurate to your needs. The CLI includes UI elements, such as an indicator in the footer showing the number of loaded context files, to keep you informed about the active context.
|
||||
|
||||
- **Purpose:** These Markdown files contain instructions, guidelines, or context that you want the Gemini model to be aware of during your interactions. The system is designed to manage this instructional context hierarchically.
|
||||
|
||||
@@ -561,13 +581,13 @@ This example demonstrates how you can provide general project context, specific
|
||||
|
||||
- **Hierarchical Loading and Precedence:** The CLI implements a sophisticated hierarchical memory system by loading context files (e.g., `GEMINI.md`) from several locations. Content from files lower in this list (more specific) typically overrides or supplements content from files higher up (more general). The exact concatenation order and final context can be inspected using the `/memory show` command. The typical loading order is:
|
||||
1. **Global Context File:**
|
||||
- Location: `~/.gemini/<contextFileName>` (e.g., `~/.gemini/GEMINI.md` in your user home directory).
|
||||
- Location: `~/.gemini/<configured-context-filename>` (e.g., `~/.gemini/GEMINI.md` in your user home directory).
|
||||
- Scope: Provides default instructions for all your projects.
|
||||
2. **Project Root & Ancestors Context Files:**
|
||||
- Location: The CLI searches for the configured context file in the current working directory and then in each parent directory up to either the project root (identified by a `.git` folder) or your home directory.
|
||||
- Scope: Provides context relevant to the entire project or a significant portion of it.
|
||||
3. **Sub-directory Context Files (Contextual/Local):**
|
||||
- Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with a `memoryDiscoveryMaxDirs` field in your `settings.json` file.
|
||||
- Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with the `context.discoveryMaxDirs` setting in your `settings.json` file.
|
||||
- Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project.
|
||||
- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt to the Gemini model. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
|
||||
- **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory Import Processor documentation](../core/memport.md).
|
||||
@@ -625,10 +645,12 @@ To help us improve the Gemini CLI, we collect anonymized usage statistics. This
|
||||
|
||||
**How to opt out:**
|
||||
|
||||
You can opt out of usage statistics collection at any time by setting the `usageStatisticsEnabled` property to `false` in your `settings.json` file:
|
||||
You can opt out of usage statistics collection at any time by setting the `usageStatisticsEnabled` property to `false` under the `privacy` category in your `settings.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"usageStatisticsEnabled": false
|
||||
"privacy": {
|
||||
"usageStatisticsEnabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+90
-43
@@ -25,8 +25,12 @@ Here is how settings from different levels are combined.
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "default-corporate-theme",
|
||||
"includeDirectories": ["/etc/gemini-cli/common-context"]
|
||||
"ui": {
|
||||
"theme": "default-corporate-theme"
|
||||
},
|
||||
"context": {
|
||||
"includeDirectories": ["/etc/gemini-cli/common-context"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -34,7 +38,9 @@ Here is how settings from different levels are combined.
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "user-preferred-dark-theme",
|
||||
"ui": {
|
||||
"theme": "user-preferred-dark-theme"
|
||||
},
|
||||
"mcpServers": {
|
||||
"corp-server": {
|
||||
"command": "/usr/local/bin/corp-server-dev"
|
||||
@@ -43,7 +49,9 @@ Here is how settings from different levels are combined.
|
||||
"command": "npm start --prefix ~/tools/my-tool"
|
||||
}
|
||||
},
|
||||
"includeDirectories": ["~/gemini-context"]
|
||||
"context": {
|
||||
"includeDirectories": ["~/gemini-context"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -51,26 +59,34 @@ Here is how settings from different levels are combined.
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "project-specific-light-theme",
|
||||
"ui": {
|
||||
"theme": "project-specific-light-theme"
|
||||
},
|
||||
"mcpServers": {
|
||||
"project-tool": {
|
||||
"command": "npm start"
|
||||
}
|
||||
},
|
||||
"includeDirectories": ["./project-context"]
|
||||
"context": {
|
||||
"includeDirectories": ["./project-context"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **System Overrides `settings.json`:**
|
||||
```json
|
||||
{
|
||||
"theme": "system-enforced-theme",
|
||||
"ui": {
|
||||
"theme": "system-enforced-theme"
|
||||
},
|
||||
"mcpServers": {
|
||||
"corp-server": {
|
||||
"command": "/usr/local/bin/corp-server-prod"
|
||||
}
|
||||
},
|
||||
"includeDirectories": ["/etc/gemini-cli/global-context"]
|
||||
"context": {
|
||||
"includeDirectories": ["/etc/gemini-cli/global-context"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,7 +95,9 @@ This results in the following merged configuration:
|
||||
- **Final Merged Configuration:**
|
||||
```json
|
||||
{
|
||||
"theme": "system-enforced-theme",
|
||||
"ui": {
|
||||
"theme": "system-enforced-theme"
|
||||
},
|
||||
"mcpServers": {
|
||||
"corp-server": {
|
||||
"command": "/usr/local/bin/corp-server-prod"
|
||||
@@ -91,12 +109,14 @@ This results in the following merged configuration:
|
||||
"command": "npm start"
|
||||
}
|
||||
},
|
||||
"includeDirectories": [
|
||||
"/etc/gemini-cli/common-context",
|
||||
"~/gemini-context",
|
||||
"./project-context",
|
||||
"/etc/gemini-cli/global-context"
|
||||
]
|
||||
"context": {
|
||||
"includeDirectories": [
|
||||
"/etc/gemini-cli/common-context",
|
||||
"~/gemini-context",
|
||||
"./project-context",
|
||||
"/etc/gemini-cli/global-context"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -117,7 +137,7 @@ By using the system settings file, you can enforce the security and configuratio
|
||||
|
||||
## Restricting Tool Access
|
||||
|
||||
You can significantly enhance security by controlling which tools the Gemini model can use. This is achieved through the `coreTools` and `excludeTools` settings. For a list of available tools, see the [Tools documentation](../tools/index.md).
|
||||
You can significantly enhance security by controlling which tools the Gemini model can use. This is achieved through the `tools.core` and `tools.exclude` settings. For a list of available tools, see the [Tools documentation](../tools/index.md).
|
||||
|
||||
### Allowlisting with `coreTools`
|
||||
|
||||
@@ -127,7 +147,9 @@ The most secure approach is to explicitly add the tools and commands that users
|
||||
|
||||
```json
|
||||
{
|
||||
"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]
|
||||
"tools": {
|
||||
"core": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -139,7 +161,9 @@ Alternatively, you can add specific tools that are considered dangerous in your
|
||||
|
||||
```json
|
||||
{
|
||||
"excludeTools": ["ShellTool(rm -rf)"]
|
||||
"tools": {
|
||||
"exclude": ["ShellTool(rm -rf)"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -172,7 +196,9 @@ Following the principle of least privilege, it is highly recommended to use `inc
|
||||
|
||||
```json
|
||||
{
|
||||
"allowMCPServers": ["third-party-analyzer"],
|
||||
"mcp": {
|
||||
"allowed": ["third-party-analyzer"]
|
||||
},
|
||||
"mcpServers": {
|
||||
"third-party-analyzer": {
|
||||
"command": "/usr/local/bin/start-3p-analyzer.sh",
|
||||
@@ -187,7 +213,7 @@ Following the principle of least privilege, it is highly recommended to use `inc
|
||||
To create a secure, centrally-managed catalog of tools, the system administrator **must** do both of the following in the system-level `settings.json` file:
|
||||
|
||||
1. **Define the full configuration** for every approved server in the `mcpServers` object. This ensures that even if a user defines a server with the same name, the secure system-level definition will take precedence.
|
||||
2. **Add the names** of those servers to an allowlist using the `allowMCPServers` setting. This is a critical security step that prevents users from running any servers that are not on this list. If this setting is omitted, the CLI will merge and allow any server defined by the user.
|
||||
2. **Add the names** of those servers to an allowlist using the `mcp.allowed` setting. This is a critical security step that prevents users from running any servers that are not on this list. If this setting is omitted, the CLI will merge and allow any server defined by the user.
|
||||
|
||||
**Example System `settings.json`:**
|
||||
|
||||
@@ -198,7 +224,9 @@ To create a secure, centrally-managed catalog of tools, the system administrator
|
||||
|
||||
```json
|
||||
{
|
||||
"allowMCPServers": ["corp-data-api", "source-code-analyzer"],
|
||||
"mcp": {
|
||||
"allowed": ["corp-data-api", "source-code-analyzer"]
|
||||
},
|
||||
"mcpServers": {
|
||||
"corp-data-api": {
|
||||
"command": "/usr/local/bin/start-corp-api.sh",
|
||||
@@ -211,16 +239,16 @@ To create a secure, centrally-managed catalog of tools, the system administrator
|
||||
}
|
||||
```
|
||||
|
||||
This pattern is more secure because it uses both definition and an allowlist. Any server a user defines will either be overridden by the system definition (if it has the same name) or blocked because its name is not in the `allowMCPServers` list.
|
||||
This pattern is more secure because it uses both definition and an allowlist. Any server a user defines will either be overridden by the system definition (if it has the same name) or blocked because its name is not in the `mcp.allowed` list.
|
||||
|
||||
### Less Secure Pattern: Omitting the Allowlist
|
||||
|
||||
If the administrator defines the `mcpServers` object but fails to also specify the `allowMCPServers` allowlist, users may add their own servers.
|
||||
If the administrator defines the `mcpServers` object but fails to also specify the `mcp.allowed` allowlist, users may add their own servers.
|
||||
|
||||
**Example System `settings.json`:**
|
||||
|
||||
This configuration defines servers but does not enforce the allowlist.
|
||||
The administrator has NOT included the "allowMCPServers" setting.
|
||||
The administrator has NOT included the "mcp.allowed" setting.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -232,7 +260,7 @@ The administrator has NOT included the "allowMCPServers" setting.
|
||||
}
|
||||
```
|
||||
|
||||
In this scenario, a user can add their own server in their local `settings.json`. Because there is no `allowMCPServers` list to filter the merged results, the user's server will be added to the list of available tools and allowed to run.
|
||||
In this scenario, a user can add their own server in their local `settings.json`. Because there is no `mcp.allowed` list to filter the merged results, the user's server will be added to the list of available tools and allowed to run.
|
||||
|
||||
## Enforcing Sandboxing for Security
|
||||
|
||||
@@ -242,7 +270,9 @@ To mitigate the risk of potentially harmful operations, you can enforce the use
|
||||
|
||||
```json
|
||||
{
|
||||
"sandbox": "docker"
|
||||
"tools": {
|
||||
"sandbox": "docker"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -287,42 +317,59 @@ For auditing and monitoring purposes, you can configure Gemini CLI to send telem
|
||||
|
||||
**Note:** Ensure that `logPrompts` is set to `false` in an enterprise setting to avoid collecting potentially sensitive information from user prompts.
|
||||
|
||||
## Authentication
|
||||
|
||||
You can enforce a specific authentication method for all users by setting the `enforcedAuthType` in the system-level `settings.json` file. This prevents users from choosing a different authentication method. See the [Authentication docs](./authentication.md) for more details.
|
||||
|
||||
**Example:** Enforce the use of Google login for all users.
|
||||
|
||||
```json
|
||||
{
|
||||
"enforcedAuthType": "oauth-personal"
|
||||
}
|
||||
```
|
||||
|
||||
If a user has a different authentication method configured, they will be prompted to switch to the enforced method. In non-interactive mode, the CLI will exit with an error if the configured authentication method does not match the enforced one.
|
||||
|
||||
## Putting It All Together: Example System `settings.json`
|
||||
|
||||
Here is an example of a system `settings.json` file that combines several of the patterns discussed above to create a secure, controlled environment for Gemini CLI.
|
||||
|
||||
```json
|
||||
{
|
||||
"sandbox": "docker",
|
||||
|
||||
"coreTools": [
|
||||
"ReadFileTool",
|
||||
"GlobTool",
|
||||
"ShellTool(ls)",
|
||||
"ShellTool(cat)",
|
||||
"ShellTool(grep)"
|
||||
],
|
||||
|
||||
"tools": {
|
||||
"sandbox": "docker",
|
||||
"core": [
|
||||
"ReadFileTool",
|
||||
"GlobTool",
|
||||
"ShellTool(ls)",
|
||||
"ShellTool(cat)",
|
||||
"ShellTool(grep)"
|
||||
]
|
||||
},
|
||||
"mcp": {
|
||||
"allowed": ["corp-tools"]
|
||||
},
|
||||
"mcpServers": {
|
||||
"corp-tools": {
|
||||
"command": "/opt/gemini-tools/start.sh",
|
||||
"timeout": 5000
|
||||
}
|
||||
},
|
||||
"allowMCPServers": ["corp-tools"],
|
||||
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp",
|
||||
"otlpEndpoint": "https://telemetry-prod.example.com:4317",
|
||||
"logPrompts": false
|
||||
},
|
||||
|
||||
"bugCommand": {
|
||||
"urlTemplate": "https://servicedesk.example.com/new-ticket?title={title}&details={info}"
|
||||
"advanced": {
|
||||
"bugCommand": {
|
||||
"urlTemplate": "https://servicedesk.example.com/new-ticket?title={title}&details={info}"
|
||||
}
|
||||
},
|
||||
|
||||
"usageStatisticsEnabled": false
|
||||
"privacy": {
|
||||
"usageStatisticsEnabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+4
-1
@@ -8,6 +8,7 @@ Within Gemini CLI, `packages/cli` is the frontend for users to send and receive
|
||||
- **[Commands](./commands.md):** A reference for Gemini CLI commands (e.g., `/help`, `/tools`, `/theme`).
|
||||
- **[Configuration](./configuration.md):** A guide to tailoring Gemini CLI behavior using configuration files.
|
||||
- **[Enterprise](./enterprise.md):** A guide to enterprise configuration.
|
||||
- **[Headless Mode](../headless.md):** A comprehensive guide to using Gemini CLI programmatically for scripting and automation.
|
||||
- **[Token Caching](./token-caching.md):** Optimize API costs through token caching.
|
||||
- **[Themes](./themes.md)**: A guide to customizing the CLI's appearance with different themes.
|
||||
- **[Tutorials](tutorials.md)**: A tutorial showing how to use Gemini CLI to automate a development task.
|
||||
@@ -22,8 +23,10 @@ The following example pipes a command to Gemini CLI from your terminal:
|
||||
echo "What is fine tuning?" | gemini
|
||||
```
|
||||
|
||||
Gemini CLI executes the command and prints the output to your terminal. Note that you can achieve the same behavior by using the `--prompt` or `-p` flag. For example:
|
||||
You can also use the `--prompt` or `-p` flag:
|
||||
|
||||
```bash
|
||||
gemini -p "What is fine tuning?"
|
||||
```
|
||||
|
||||
For comprehensive documentation on headless usage, scripting, automation, and advanced examples, see the **[Headless Mode](../headless.md)** guide.
|
||||
|
||||
+12
-21
@@ -46,25 +46,14 @@ Add a `customThemes` block to your user, project, or system `settings.json` file
|
||||
|
||||
```json
|
||||
{
|
||||
"customThemes": {
|
||||
"MyCustomTheme": {
|
||||
"name": "MyCustomTheme",
|
||||
"type": "custom",
|
||||
"Background": "#181818",
|
||||
"Foreground": "#F8F8F2",
|
||||
"LightBlue": "#82AAFF",
|
||||
"AccentBlue": "#61AFEF",
|
||||
"AccentPurple": "#C678DD",
|
||||
"AccentCyan": "#56B6C2",
|
||||
"AccentGreen": "#98C379",
|
||||
"AccentYellow": "#E5C07B",
|
||||
"AccentRed": "#E06C75",
|
||||
"Comment": "#5C6370",
|
||||
"Gray": "#ABB2BF",
|
||||
"DiffAdded": "#A6E3A1",
|
||||
"DiffRemoved": "#F38BA8",
|
||||
"DiffModified": "#89B4FA",
|
||||
"GradientColors": ["#4796E4", "#847ACE", "#C3677F"]
|
||||
"ui": {
|
||||
"customThemes": {
|
||||
"MyCustomTheme": {
|
||||
"name": "MyCustomTheme",
|
||||
"type": "custom",
|
||||
"Background": "#181818",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +104,9 @@ To load a theme from a file, set the `theme` property in your `settings.json` to
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "/path/to/your/theme.json"
|
||||
"ui": {
|
||||
"theme": "/path/to/your/theme.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -154,7 +145,7 @@ The theme file must be a valid JSON file that follows the same structure as a cu
|
||||
### Using Your Custom Theme
|
||||
|
||||
- Select your custom theme using the `/theme` command in Gemini CLI. Your custom theme will appear in the theme selection dialog.
|
||||
- Or, set it as the default by adding `"theme": "MyCustomTheme"` to your `settings.json`.
|
||||
- Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui` object in your `settings.json`.
|
||||
- Custom themes can be set at the user, project, or system level, and follow the same [configuration precedence](./configuration.md) as other settings.
|
||||
|
||||
---
|
||||
|
||||
@@ -147,7 +147,7 @@ Processes import statements in GEMINI.md content.
|
||||
- `debugMode` (boolean, optional): Whether to enable debug logging (default: false)
|
||||
- `importState` (ImportState, optional): State tracking for circular import prevention
|
||||
|
||||
**Returns:** Promise<ProcessImportsResult> - Object containing processed content and import tree
|
||||
**Returns:** Promise<ProcessImportsResult> - Object containing processed content and import tree
|
||||
|
||||
### `ProcessImportsResult`
|
||||
|
||||
@@ -187,7 +187,7 @@ Finds the project root by searching for a `.git` directory upwards from the give
|
||||
|
||||
- `startDir` (string): The directory to start searching from
|
||||
|
||||
**Returns:** Promise<string> - The project root directory (or the start directory if no `.git` is found)
|
||||
**Returns:** Promise<string> - The project root directory (or the start directory if no `.git` is found)
|
||||
|
||||
## Best Practices
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ The Gemini CLI core (`packages/core`) features a robust system for defining, reg
|
||||
- **Tool Registry (`tool-registry.ts`):** A class (`ToolRegistry`) responsible for:
|
||||
- **Registering Tools:** Holding a collection of all available built-in tools (e.g., `ReadFileTool`, `ShellTool`).
|
||||
- **Discovering Tools:** It can also discover tools dynamically:
|
||||
- **Command-based Discovery:** If `toolDiscoveryCommand` is configured in settings, this command is executed. It's expected to output JSON describing custom tools, which are then registered as `DiscoveredTool` instances.
|
||||
- **MCP-based Discovery:** If `mcpServerCommand` is configured, the registry can connect to a Model Context Protocol (MCP) server to list and register tools (`DiscoveredMCPTool`).
|
||||
- **Command-based Discovery:** If `tools.discoveryCommand` is configured in settings, this command is executed. It's expected to output JSON describing custom tools, which are then registered as `DiscoveredTool` instances.
|
||||
- **MCP-based Discovery:** If `mcp.serverCommand` is configured, the registry can connect to a Model Context Protocol (MCP) server to list and register tools (`DiscoveredMCPTool`).
|
||||
- **Providing Schemas:** Exposing the `FunctionDeclaration` schemas of all registered tools to the Gemini model, so it knows what tools are available and how to use them.
|
||||
- **Retrieving Tools:** Allowing the core to get a specific tool by name for execution.
|
||||
|
||||
@@ -69,7 +69,7 @@ Each of these tools extends `BaseTool` and implements the required methods for i
|
||||
|
||||
While direct programmatic registration of new tools by users isn't explicitly detailed as a primary workflow in the provided files for typical end-users, the architecture supports extension through:
|
||||
|
||||
- **Command-based Discovery:** Advanced users or project administrators can define a `toolDiscoveryCommand` in `settings.json`. This command, when run by the Gemini CLI core, should output a JSON array of `FunctionDeclaration` objects. The core will then make these available as `DiscoveredTool` instances. The corresponding `toolCallCommand` would then be responsible for actually executing these custom tools.
|
||||
- **Command-based Discovery:** Advanced users or project administrators can define a `tools.discoveryCommand` in `settings.json`. This command, when run by the Gemini CLI core, should output a JSON array of `FunctionDeclaration` objects. The core will then make these available as `DiscoveredTool` instances. The corresponding `tools.callCommand` would then be responsible for actually executing these custom tools.
|
||||
- **MCP Server(s):** For more complex scenarios, one or more MCP servers can be set up and configured via the `mcpServers` setting in `settings.json`. The Gemini CLI core can then discover and use tools exposed by these servers. As mentioned, if you have multiple MCP servers, the tool names will be prefixed with the server name from your configuration (e.g., `serverAlias__actualToolName`).
|
||||
|
||||
This tool system provides a flexible and powerful way to augment the Gemini model's capabilities, making the Gemini CLI a versatile assistant for a wide range of tasks.
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# Extension Releasing
|
||||
|
||||
There are two primary ways of releasing extensions to users:
|
||||
|
||||
- [Git repository](#releasing-through-a-git-repository)
|
||||
- [Github Releases](#releasing-through-github-releases)
|
||||
|
||||
Git repository releases tend to be the simplest and most flexible approach, while GitHub releases can be more efficient on initial install as they are shipped as single archives instead of requiring a git clone which downloads each file individually. Github releases may also contain platform specific archives if you need to ship platform specific binary files.
|
||||
|
||||
## Releasing through a git repository
|
||||
|
||||
This is the most flexible and simple option. All you need to do us create a publicly accessible git repo (such as a public github repository) and then users can install your extension using `gemini extensions install <your-repo-uri>`, or for a GitHub repository they can use the simplified `gemini extensions install <org>/<repo>` format. They can optionally depend on a specific ref (branch/tag/commit) using the `--ref=<some-ref>` argument, this defaults to the default branch.
|
||||
|
||||
Whenever commits are pushed to the ref that a user depends on, they will be prompted to update the extension. Note that this also allows for easy rollbacks, the HEAD commit is always treated as the latest version regardless of the actual version in the `gemini-extension.json` file.
|
||||
|
||||
### Managing release channels using a git repository
|
||||
|
||||
Users can depend on any ref from your git repo, such as a branch or tag, which allows you to manage multiple release channels.
|
||||
|
||||
For instance, you can maintain a `stable` branch, which users can install this way `gemini extensions install <your-repo-uri> --ref=stable`. Or, you could make this the default by treating your default branch as your stable release branch, and doing development in a different branch (for instance called `dev`). You can maintain as many branches or tags as you like, providing maximum flexibility for you and your users.
|
||||
|
||||
Note that these `ref` arguments can be tags, branches, or even specific commits, which allows users to depend on a specific version of your extension. It is up to you how you want to manage your tags and branches.
|
||||
|
||||
### Example releasing flow using a git repo
|
||||
|
||||
While there are many options for how you want to manage releases using a git flow, we recommend treating your default branch as your "stable" release branch. This means that the default behavior for `gemini extensions install <your-repo-uri>` is to be on the stable release branch.
|
||||
|
||||
Lets say you want to maintain three standard release channels, `stable`, `preview`, and `dev`. You would do all your standard development in the `dev` branch. When you are ready to do a preview release, you merge that branch into your `preview` branch. When you are ready to promote your preview branch to stable, you merge `preview` into your stable branch (which might be your default branch or a different branch).
|
||||
|
||||
You can also cherry pick changes from one branch into another using `git cherry-pick`, but do note that this will result in your branches having a slightly divergent history from each other, unless you force push changes to your branches on each release to restore the history to a clean slate (which may not be possible for the default branch depending on your repository settings). If you plan on doing cherry picks, you may want to avoid having your default branch be the stable branch to avoid force-pushing to the default branch which should generally be avoided.
|
||||
|
||||
## Releasing through Github releases
|
||||
|
||||
Gemini CLI extensions can be distributed through [GitHub Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases). This provides a faster and more reliable initial installation experience for users, as it avoids the need to clone the repository.
|
||||
|
||||
Each release includes at least one archive file, which contains the full contents of the repo at the tag that it was linked to. Releases may also include [pre-built archives](#custom-pre-built-archives) if your extension requires some build step or has platform specific binaries attached to it.
|
||||
|
||||
When checking for updates, gemini will just look for the latest release on github (you must mark it as such when creating the release), unless the user installed a specific release by passing `--ref=<some-release-tag>`. We do not at this time support opting in to pre-release releases or semver.
|
||||
|
||||
### Custom pre-built archives
|
||||
|
||||
Custom archives must be attached directly to the github release as assets and must be fully self-contained. This means they should include the entire extension, see [archive structure](#archive-structure).
|
||||
|
||||
If your extension is platform-independent, you can provide a single generic asset. In this case, there should be only one asset attached to the release.
|
||||
|
||||
Custom archives may also be used if you want to develop your extension within a larger repository, you can build an archive which has a different layout from the repo itself (for instance it might just be an archive of a subdirectory containing the extension).
|
||||
|
||||
#### Platform specific archives
|
||||
|
||||
To ensure Gemini CLI can automatically find the correct release asset for each platform, you must follow this naming convention. The CLI will search for assets in the following order:
|
||||
|
||||
1. **Platform and Architecture-Specific:** `{platform}.{arch}.{name}.{extension}`
|
||||
2. **Platform-Specific:** `{platform}.{name}.{extension}`
|
||||
3. **Generic:** If only one asset is provided, it will be used as a generic fallback.
|
||||
|
||||
- `{name}`: The name of your extension.
|
||||
- `{platform}`: The operating system. Supported values are:
|
||||
- `darwin` (macOS)
|
||||
- `linux`
|
||||
- `win32` (Windows)
|
||||
- `{arch}`: The architecture. Supported values are:
|
||||
- `x64`
|
||||
- `arm64`
|
||||
- `{extension}`: The file extension of the archive (e.g., `.tar.gz` or `.zip`).
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `darwin.arm64.my-tool.tar.gz` (specific to Apple Silicon Macs)
|
||||
- `darwin.my-tool.tar.gz` (for all Macs)
|
||||
- `linux.x64.my-tool.tar.gz`
|
||||
- `win32.my-tool.zip`
|
||||
|
||||
#### Archive structure
|
||||
|
||||
Archives must be fully contained extensions and have all the standard requirements - specifically the `gemini-extension.json` file must be at the root of the archive.
|
||||
|
||||
The rest of the layout should look exactly the same as a typical extension, see [extensions.md](extension.md).
|
||||
|
||||
#### Example GitHub Actions workflow
|
||||
|
||||
Here is an example of a GitHub Actions workflow that builds and releases a Gemini CLI extension for multiple platforms:
|
||||
|
||||
```yaml
|
||||
name: Release Extension
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build
|
||||
|
||||
- name: Create release assets
|
||||
run: |
|
||||
npm run package -- --platform=darwin --arch=arm64
|
||||
npm run package -- --platform=linux --arch=x64
|
||||
npm run package -- --platform=win32 --arch=x64
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
release/darwin.arm64.my-tool.tar.gz
|
||||
release/linux.arm64.my-tool.tar.gz
|
||||
release/win32.arm64.my-tool.zip
|
||||
```
|
||||
+89
-29
@@ -1,19 +1,90 @@
|
||||
# Gemini CLI Extensions
|
||||
|
||||
Gemini CLI supports extensions that can be used to configure and extend its functionality.
|
||||
_This documentation is up-to-date with the v0.4.0 release._
|
||||
|
||||
Gemini CLI extensions package prompts, MCP servers, and custom commands into a familiar and user-friendly format. With extensions, you can expand the capabilities of Gemini CLI and share those capabilities with others. They are designed to be easily installable and shareable.
|
||||
|
||||
## Extension management
|
||||
|
||||
We offer a suite of extension management tools using `gemini extensions` commands.
|
||||
|
||||
Note that these commands are not supported from within the CLI, although you can list installed extensions using the `/extensions list` subcommand.
|
||||
|
||||
Note that all of these commands will only be reflected in active CLI sessions on restart.
|
||||
|
||||
### Installing an extension
|
||||
|
||||
You can install an extension using `gemini extensions install` with either a GitHub URL source or `--path=some/local/path`.
|
||||
|
||||
Note that we create a copy of the installed extension, so you will need to run `gemini extensions update` to pull in changes from both locally-defined extensions and those on GitHub.
|
||||
|
||||
```
|
||||
gemini extensions install https://github.com/gemini-cli-extensions/security
|
||||
```
|
||||
|
||||
This will install the Gemini CLI Security extension, which offers support for a `/security:analyze` command.
|
||||
|
||||
### Uninstalling an extension
|
||||
|
||||
To uninstall, run `gemini extensions uninstall extension-name`, so, in the case of the install example:
|
||||
|
||||
```
|
||||
gemini extensions uninstall gemini-cli-security
|
||||
```
|
||||
|
||||
### Disabling an extension
|
||||
|
||||
Extensions are, by default, enabled across all workspaces. You can disable an extension entirely or for specific workspace.
|
||||
|
||||
For example, `gemini extensions disable extension-name` will disable the extension at the user level, so it will be disabled everywhere. `gemini extensions disable extension-name --scope=workspace` will only disable the extension in the current workspace.
|
||||
|
||||
### Enabling an extension
|
||||
|
||||
You can enable extensions using `gemini extensions enable extension-name`. You can also enable an extension for a specific workspace using `gemini extensions enable extension-name --scope=workspace` from within that workspace.
|
||||
|
||||
This is useful if you have an extension disabled at the top-level and only enabled in specific places.
|
||||
|
||||
### Updating an extension
|
||||
|
||||
For extensions installed from a local path or a git repository, you can explicitly update to the latest version (as reflected in the `gemini-extension.json` `version` field) with `gemini extensions update extension-name`.
|
||||
|
||||
You can update all extensions with:
|
||||
|
||||
```
|
||||
gemini extensions update --all
|
||||
```
|
||||
|
||||
## Extension creation
|
||||
|
||||
We offer commands to make extension development easier.
|
||||
|
||||
### Create a boilerplate extension
|
||||
|
||||
We offer several example extensions `context`, `custom-commands`, `exclude-tools` and `mcp-server`. You can view these examples [here](https://github.com/google-gemini/gemini-cli/tree/main/packages/cli/src/commands/extensions/examples).
|
||||
|
||||
To copy one of these examples into a development directory using the type of your choosing, run:
|
||||
|
||||
```
|
||||
gemini extensions new path/to/directory custom-commands
|
||||
```
|
||||
|
||||
### Link a local extension
|
||||
|
||||
The `gemini extensions link` command will create a symbolic link from the extension installation directory to the development path.
|
||||
|
||||
This is useful so you don't have to run `gemini extensions update` every time you make changes you'd like to test.
|
||||
|
||||
```
|
||||
gemini extensions link path/to/directory
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
On startup, Gemini CLI looks for extensions in two locations:
|
||||
On startup, Gemini CLI looks for extensions in `<home>/.gemini/extensions`
|
||||
|
||||
1. `<workspace>/.gemini/extensions`
|
||||
2. `<home>/.gemini/extensions`
|
||||
Extensions exist as a directory that contains a `gemini-extension.json` file. For example:
|
||||
|
||||
Gemini CLI loads all extensions from both locations. If an extension with the same name exists in both locations, the extension in the workspace directory takes precedence.
|
||||
|
||||
Within each location, individual extensions exist as a directory that contains a `gemini-extension.json` file. For example:
|
||||
|
||||
`<workspace>/.gemini/extensions/my-extension/gemini-extension.json`
|
||||
`<home>/.gemini/extensions/my-extension/gemini-extension.json`
|
||||
|
||||
### `gemini-extension.json`
|
||||
|
||||
@@ -33,19 +104,20 @@ The `gemini-extension.json` file contains the configuration for the extension. T
|
||||
}
|
||||
```
|
||||
|
||||
- `name`: The name of the extension. This is used to uniquely identify the extension and for conflict resolution when extension commands have the same name as user or project commands.
|
||||
- `name`: The name of the extension. This is used to uniquely identify the extension and for conflict resolution when extension commands have the same name as user or project commands. The name should be lowercase or numbers and use dashes instead of underscores or spaces. This is how users will refer to your extension in the CLI. Note that we expect this name to match the extension directory name.
|
||||
- `version`: The version of the extension.
|
||||
- `mcpServers`: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a [`settings.json` file](./cli/configuration.md). If both an extension and a `settings.json` file configure an MCP server with the same name, the server defined in the `settings.json` file takes precedence.
|
||||
- `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the workspace. If this property is not used but a `GEMINI.md` file is present in your extension directory, then that file will be loaded.
|
||||
- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command.
|
||||
- Note that all MCP server configuration options are supported except for `trust`.
|
||||
- `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the extension directory. If this property is not used but a `GEMINI.md` file is present in your extension directory, then that file will be loaded.
|
||||
- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command. Note that this differs from the MCP server `excludeTools` functionality, which can be listed in the MCP server config.
|
||||
|
||||
When Gemini CLI starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence.
|
||||
|
||||
## Extension Commands
|
||||
### Custom commands
|
||||
|
||||
Extensions can provide [custom commands](./cli/commands.md#custom-commands) by placing TOML files in a `commands/` subdirectory within the extension directory. These commands follow the same format as user and project custom commands and use standard naming conventions.
|
||||
|
||||
### Example
|
||||
**Example**
|
||||
|
||||
An extension named `gcp` with the following structure:
|
||||
|
||||
@@ -63,7 +135,7 @@ Would provide these commands:
|
||||
- `/deploy` - Shows as `[gcp] Custom command from deploy.toml` in help
|
||||
- `/gcs:sync` - Shows as `[gcp] Custom command from sync.toml` in help
|
||||
|
||||
### Conflict Resolution
|
||||
### Conflict resolution
|
||||
|
||||
Extension commands have the lowest precedence. When a conflict occurs with user or project commands:
|
||||
|
||||
@@ -75,20 +147,7 @@ For example, if both a user and the `gcp` extension define a `deploy` command:
|
||||
- `/deploy` - Executes the user's deploy command
|
||||
- `/gcp.deploy` - Executes the extension's deploy command (marked with `[gcp]` tag)
|
||||
|
||||
## Installing Extensions
|
||||
|
||||
You can install extensions using the `install` command. This command allows you to install extensions from a Git repository or a local path.
|
||||
|
||||
### Usage
|
||||
|
||||
`gemini extensions install <source> | [options]`
|
||||
|
||||
### Options
|
||||
|
||||
- `source <url> positional argument`: The URL of a Git repository to install the extension from. The repository must contain a `gemini-extension.json` file in its root.
|
||||
- `--path <path>`: The path to a local directory to install as an extension. The directory must contain a `gemini-extension.json` file.
|
||||
|
||||
# Variables
|
||||
## Variables
|
||||
|
||||
Gemini CLI extensions allow variable substitution in `gemini-extension.json`. This can be useful if e.g., you need the current directory to run an MCP server using `"cwd": "${extensionPath}${/}run.ts"`.
|
||||
|
||||
@@ -97,4 +156,5 @@ Gemini CLI extensions allow variable substitution in `gemini-extension.json`. Th
|
||||
| variable | description |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `${extensionPath}` | The fully-qualified path of the extension in the user's filesystem e.g., '/Users/username/.gemini/extensions/example-extension'. This will not unwrap symlinks. |
|
||||
| `${workspacePath}` | The fully-qualified path of the current workspace. |
|
||||
| `${/} or ${pathSeparator}` | The path separator (differs per OS). |
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Getting Started with Gemini CLI Extensions
|
||||
|
||||
This guide will walk you through creating your first Gemini CLI extension. You'll learn how to set up a new extension, add a custom tool via an MCP server, create a custom command, and provide context to the model with a `GEMINI.md` file.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start, make sure you have the Gemini CLI installed and a basic understanding of Node.js and TypeScript.
|
||||
|
||||
## Step 1: Create a New Extension
|
||||
|
||||
The easiest way to start is by using one of the built-in templates. We'll use the `mcp-server` example as our foundation.
|
||||
|
||||
Run the following command to create a new directory called `my-first-extension` with the template files:
|
||||
|
||||
```bash
|
||||
gemini extensions new my-first-extension mcp-server
|
||||
```
|
||||
|
||||
This will create a new directory with the following structure:
|
||||
|
||||
```
|
||||
my-first-extension/
|
||||
├── example.ts
|
||||
├── gemini-extension.json
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
## Step 2: Understand the Extension Files
|
||||
|
||||
Let's look at the key files in your new extension.
|
||||
|
||||
### `gemini-extension.json`
|
||||
|
||||
This is the manifest file for your extension. It tells Gemini CLI how to load and use your extension.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-first-extension",
|
||||
"version": "1.0.0",
|
||||
"mcpServers": {
|
||||
"nodeServer": {
|
||||
"command": "node",
|
||||
"args": ["${extensionPath}${/}dist${/}example.js"],
|
||||
"cwd": "${extensionPath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `name`: The unique name for your extension.
|
||||
- `version`: The version of your extension.
|
||||
- `mcpServers`: This section defines one or more Model Context Protocol (MCP) servers. MCP servers are how you can add new tools for the model to use.
|
||||
- `command`, `args`, `cwd`: These fields specify how to start your server. Notice the use of the `${extensionPath}` variable, which Gemini CLI replaces with the absolute path to your extension's installation directory. This allows your extension to work regardless of where it's installed.
|
||||
|
||||
### `example.ts`
|
||||
|
||||
This file contains the source code for your MCP server. It's a simple Node.js server that uses the `@modelcontextprotocol/sdk`.
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'prompt-server',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// Registers a new tool named 'fetch_posts'
|
||||
server.registerTool(
|
||||
'fetch_posts',
|
||||
{
|
||||
description: 'Fetches a list of posts from a public API.',
|
||||
inputSchema: z.object({}).shape,
|
||||
},
|
||||
async () => {
|
||||
const apiResponse = await fetch(
|
||||
'https://jsonplaceholder.typicode.com/posts',
|
||||
);
|
||||
const posts = await apiResponse.json();
|
||||
const response = { posts: posts.slice(0, 5) };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ... (prompt registration omitted for brevity)
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
This server defines a single tool called `fetch_posts` that fetches data from a public API.
|
||||
|
||||
### `package.json` and `tsconfig.json`
|
||||
|
||||
These are standard configuration files for a TypeScript project. The `package.json` file defines dependencies and a `build` script, and `tsconfig.json` configures the TypeScript compiler.
|
||||
|
||||
## Step 3: Build and Link Your Extension
|
||||
|
||||
Before you can use the extension, you need to compile the TypeScript code and link the extension to your Gemini CLI installation for local development.
|
||||
|
||||
1. **Install dependencies:**
|
||||
|
||||
```bash
|
||||
cd my-first-extension
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Build the server:**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This will compile `example.ts` into `dist/example.js`, which is the file referenced in your `gemini-extension.json`.
|
||||
|
||||
3. **Link the extension:**
|
||||
|
||||
The `link` command creates a symbolic link from the Gemini CLI extensions directory to your development directory. This means any changes you make will be reflected immediately without needing to reinstall.
|
||||
|
||||
```bash
|
||||
gemini extensions link .
|
||||
```
|
||||
|
||||
Now, restart your Gemini CLI session. The new `fetch_posts` tool will be available. You can test it by asking: "fetch posts".
|
||||
|
||||
## Step 4: Add a Custom Command
|
||||
|
||||
Custom commands provide a way to create shortcuts for complex prompts. Let's add a command that searches for a pattern in your code.
|
||||
|
||||
1. Create a `commands` directory and a subdirectory for your command group:
|
||||
|
||||
```bash
|
||||
mkdir -p commands/fs
|
||||
```
|
||||
|
||||
2. Create a file named `commands/fs/grep-code.toml`:
|
||||
|
||||
```toml
|
||||
prompt = """
|
||||
Please summarize the findings for the pattern `{{args}}`.
|
||||
|
||||
Search Results:
|
||||
!{grep -r {{args}} .}
|
||||
"""
|
||||
```
|
||||
|
||||
This command, `/fs:grep-code`, will take an argument, run the `grep` shell command with it, and pipe the results into a prompt for summarization.
|
||||
|
||||
After saving the file, restart the Gemini CLI. You can now run `/fs:grep-code "some pattern"` to use your new command.
|
||||
|
||||
## Step 5: Add a Custom `GEMINI.md`
|
||||
|
||||
You can provide persistent context to the model by adding a `GEMINI.md` file to your extension. This is useful for giving the model instructions on how to behave or information about your extension's tools. Note that you may not always need this for extensions built to expose commands and prompts.
|
||||
|
||||
1. Create a file named `GEMINI.md` in the root of your extension directory:
|
||||
|
||||
```markdown
|
||||
# My First Extension Instructions
|
||||
|
||||
You are an expert developer assistant. When the user asks you to fetch posts, use the `fetch_posts` tool. Be concise in your responses.
|
||||
```
|
||||
|
||||
2. Update your `gemini-extension.json` to tell the CLI to load this file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-first-extension",
|
||||
"version": "1.0.0",
|
||||
"contextFileName": "GEMINI.md",
|
||||
"mcpServers": {
|
||||
"nodeServer": {
|
||||
"command": "node",
|
||||
"args": ["${extensionPath}${/}dist${/}example.js"],
|
||||
"cwd": "${extensionPath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart the CLI again. The model will now have the context from your `GEMINI.md` file in every session where the extension is active.
|
||||
|
||||
## Step 6: Releasing Your Extension
|
||||
|
||||
Once you are happy with your extension, you can share it with others. The two primary ways of releasing extensions are via a Git repository or through GitHub Releases. Using a public Git repository is the simplest method.
|
||||
|
||||
For detailed instructions on both methods, please refer to the [Extension Releasing Guide](extension-releasing.md).
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've successfully created a Gemini CLI extension! You learned how to:
|
||||
|
||||
- Bootstrap a new extension from a template.
|
||||
- Add custom tools with an MCP server.
|
||||
- Create convenient custom commands.
|
||||
- Provide persistent context to the model.
|
||||
- Link your extension for local development.
|
||||
|
||||
From here, you can explore more advanced features and build powerful new capabilities into the Gemini CLI.
|
||||
@@ -0,0 +1,323 @@
|
||||
# Headless Mode
|
||||
|
||||
Headless mode allows you to run Gemini CLI programmatically from command line
|
||||
scripts and automation tools without any interactive UI. This is ideal for
|
||||
scripting, automation, CI/CD pipelines, and building AI-powered tools.
|
||||
|
||||
- [Headless Mode](#headless-mode)
|
||||
- [Overview](#overview)
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [Direct Prompts](#direct-prompts)
|
||||
- [Stdin Input](#stdin-input)
|
||||
- [Combining with File Input](#combining-with-file-input)
|
||||
- [Output Formats](#output-formats)
|
||||
- [Text Output (Default)](#text-output-default)
|
||||
- [JSON Output](#json-output)
|
||||
- [Response Schema](#response-schema)
|
||||
- [Example Usage](#example-usage)
|
||||
- [File Redirection](#file-redirection)
|
||||
- [Configuration Options](#configuration-options)
|
||||
- [Examples](#examples)
|
||||
- [Code review](#code-review)
|
||||
- [Generate commit messages](#generate-commit-messages)
|
||||
- [API documentation](#api-documentation)
|
||||
- [Batch code analysis](#batch-code-analysis)
|
||||
- [Code review](#code-review-1)
|
||||
- [Log analysis](#log-analysis)
|
||||
- [Release notes generation](#release-notes-generation)
|
||||
- [Model and tool usage tracking](#model-and-tool-usage-tracking)
|
||||
- [Resources](#resources)
|
||||
|
||||
## Overview
|
||||
|
||||
The headless mode provides a headless interface to Gemini CLI that:
|
||||
|
||||
- Accepts prompts via command line arguments or stdin
|
||||
- Returns structured output (text or JSON)
|
||||
- Supports file redirection and piping
|
||||
- Enables automation and scripting workflows
|
||||
- Provides consistent exit codes for error handling
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Direct Prompts
|
||||
|
||||
Use the `--prompt` (or `-p`) flag to run in headless mode:
|
||||
|
||||
```bash
|
||||
gemini --prompt "What is machine learning?"
|
||||
```
|
||||
|
||||
### Stdin Input
|
||||
|
||||
Pipe input to Gemini CLI from your terminal:
|
||||
|
||||
```bash
|
||||
echo "Explain this code" | gemini
|
||||
```
|
||||
|
||||
### Combining with File Input
|
||||
|
||||
Read from files and process with Gemini:
|
||||
|
||||
```bash
|
||||
cat README.md | gemini --prompt "Summarize this documentation"
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
### Text Output (Default)
|
||||
|
||||
Standard human-readable output:
|
||||
|
||||
```bash
|
||||
gemini -p "What is the capital of France?"
|
||||
```
|
||||
|
||||
Response format:
|
||||
|
||||
```
|
||||
The capital of France is Paris.
|
||||
```
|
||||
|
||||
### JSON Output
|
||||
|
||||
Returns structured data including response, statistics, and metadata. This
|
||||
format is ideal for programmatic processing and automation scripts.
|
||||
|
||||
#### Response Schema
|
||||
|
||||
The JSON output follows this high-level structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"response": "string", // The main AI-generated content answering your prompt
|
||||
"stats": {
|
||||
// Usage metrics and performance data
|
||||
"models": {
|
||||
// Per-model API and token usage statistics
|
||||
"[model-name]": {
|
||||
"api": {
|
||||
/* request counts, errors, latency */
|
||||
},
|
||||
"tokens": {
|
||||
/* prompt, response, cached, total counts */
|
||||
}
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
// Tool execution statistics
|
||||
"totalCalls": "number",
|
||||
"totalSuccess": "number",
|
||||
"totalFail": "number",
|
||||
"totalDurationMs": "number",
|
||||
"totalDecisions": {
|
||||
/* accept, reject, modify, auto_accept counts */
|
||||
},
|
||||
"byName": {
|
||||
/* per-tool detailed stats */
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
// File modification statistics
|
||||
"totalLinesAdded": "number",
|
||||
"totalLinesRemoved": "number"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
// Present only when an error occurred
|
||||
"type": "string", // Error type (e.g., "ApiError", "AuthError")
|
||||
"message": "string", // Human-readable error description
|
||||
"code": "number" // Optional error code
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Usage
|
||||
|
||||
```bash
|
||||
gemini -p "What is the capital of France?" --output-format json
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"response": "The capital of France is Paris.",
|
||||
"stats": {
|
||||
"models": {
|
||||
"gemini-2.5-pro": {
|
||||
"api": {
|
||||
"totalRequests": 2,
|
||||
"totalErrors": 0,
|
||||
"totalLatencyMs": 5053
|
||||
},
|
||||
"tokens": {
|
||||
"prompt": 24939,
|
||||
"candidates": 20,
|
||||
"total": 25113,
|
||||
"cached": 21263,
|
||||
"thoughts": 154,
|
||||
"tool": 0
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash": {
|
||||
"api": {
|
||||
"totalRequests": 1,
|
||||
"totalErrors": 0,
|
||||
"totalLatencyMs": 1879
|
||||
},
|
||||
"tokens": {
|
||||
"prompt": 8965,
|
||||
"candidates": 10,
|
||||
"total": 9033,
|
||||
"cached": 0,
|
||||
"thoughts": 30,
|
||||
"tool": 28
|
||||
}
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"totalCalls": 1,
|
||||
"totalSuccess": 1,
|
||||
"totalFail": 0,
|
||||
"totalDurationMs": 1881,
|
||||
"totalDecisions": {
|
||||
"accept": 0,
|
||||
"reject": 0,
|
||||
"modify": 0,
|
||||
"auto_accept": 1
|
||||
},
|
||||
"byName": {
|
||||
"google_web_search": {
|
||||
"count": 1,
|
||||
"success": 1,
|
||||
"fail": 0,
|
||||
"durationMs": 1881,
|
||||
"decisions": {
|
||||
"accept": 0,
|
||||
"reject": 0,
|
||||
"modify": 0,
|
||||
"auto_accept": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"totalLinesAdded": 0,
|
||||
"totalLinesRemoved": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File Redirection
|
||||
|
||||
Save output to files or pipe to other commands:
|
||||
|
||||
```bash
|
||||
# Save to file
|
||||
gemini -p "Explain Docker" > docker-explanation.txt
|
||||
gemini -p "Explain Docker" --output-format json > docker-explanation.json
|
||||
|
||||
# Append to file
|
||||
gemini -p "Add more details" >> docker-explanation.txt
|
||||
|
||||
# Pipe to other tools
|
||||
gemini -p "What is Kubernetes?" --output-format json | jq '.response'
|
||||
gemini -p "Explain microservices" | wc -w
|
||||
gemini -p "List programming languages" | grep -i "python"
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Key command-line options for headless usage:
|
||||
|
||||
| Option | Description | Example |
|
||||
| ----------------------- | ---------------------------------- | -------------------------------------------------- |
|
||||
| `--prompt`, `-p` | Run in headless mode | `gemini -p "query"` |
|
||||
| `--output-format` | Specify output format (text, json) | `gemini -p "query" --output-format json` |
|
||||
| `--model`, `-m` | Specify the Gemini model | `gemini -p "query" -m gemini-2.5-flash` |
|
||||
| `--debug`, `-d` | Enable debug mode | `gemini -p "query" --debug` |
|
||||
| `--all-files`, `-a` | Include all files in context | `gemini -p "query" --all-files` |
|
||||
| `--include-directories` | Include additional directories | `gemini -p "query" --include-directories src,docs` |
|
||||
| `--yolo`, `-y` | Auto-approve all actions | `gemini -p "query" --yolo` |
|
||||
| `--approval-mode` | Set approval mode | `gemini -p "query" --approval-mode auto_edit` |
|
||||
|
||||
For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](./cli/configuration.md).
|
||||
|
||||
## Examples
|
||||
|
||||
#### Code review
|
||||
|
||||
```bash
|
||||
cat src/auth.py | gemini -p "Review this authentication code for security issues" > security-review.txt
|
||||
```
|
||||
|
||||
#### Generate commit messages
|
||||
|
||||
```bash
|
||||
result=$(git diff --cached | gemini -p "Write a concise commit message for these changes" --output-format json)
|
||||
echo "$result" | jq -r '.response'
|
||||
```
|
||||
|
||||
#### API documentation
|
||||
|
||||
```bash
|
||||
result=$(cat api/routes.js | gemini -p "Generate OpenAPI spec for these routes" --output-format json)
|
||||
echo "$result" | jq -r '.response' > openapi.json
|
||||
```
|
||||
|
||||
#### Batch code analysis
|
||||
|
||||
```bash
|
||||
for file in src/*.py; do
|
||||
echo "Analyzing $file..."
|
||||
result=$(cat "$file" | gemini -p "Find potential bugs and suggest improvements" --output-format json)
|
||||
echo "$result" | jq -r '.response' > "reports/$(basename "$file").analysis"
|
||||
echo "Completed analysis for $(basename "$file")" >> reports/progress.log
|
||||
done
|
||||
```
|
||||
|
||||
#### Code review
|
||||
|
||||
```bash
|
||||
result=$(git diff origin/main...HEAD | gemini -p "Review these changes for bugs, security issues, and code quality" --output-format json)
|
||||
echo "$result" | jq -r '.response' > pr-review.json
|
||||
```
|
||||
|
||||
#### Log analysis
|
||||
|
||||
```bash
|
||||
grep "ERROR" /var/log/app.log | tail -20 | gemini -p "Analyze these errors and suggest root cause and fixes" > error-analysis.txt
|
||||
```
|
||||
|
||||
#### Release notes generation
|
||||
|
||||
```bash
|
||||
result=$(git log --oneline v1.0.0..HEAD | gemini -p "Generate release notes from these commits" --output-format json)
|
||||
response=$(echo "$result" | jq -r '.response')
|
||||
echo "$response"
|
||||
echo "$response" >> CHANGELOG.md
|
||||
```
|
||||
|
||||
#### Model and tool usage tracking
|
||||
|
||||
```bash
|
||||
result=$(gemini -p "Explain this database schema" --include-directories db --output-format json)
|
||||
total_tokens=$(echo "$result" | jq -r '.stats.models // {} | to_entries | map(.value.tokens.total) | add // 0')
|
||||
models_used=$(echo "$result" | jq -r '.stats.models // {} | keys | join(", ") | if . == "" then "none" else . end')
|
||||
tool_calls=$(echo "$result" | jq -r '.stats.tools.totalCalls // 0')
|
||||
tools_used=$(echo "$result" | jq -r '.stats.tools.byName // {} | keys | join(", ") | if . == "" then "none" else . end')
|
||||
echo "$(date): $total_tokens tokens, $tool_calls tool calls ($tools_used) used with models: $models_used" >> usage.log
|
||||
echo "$result" | jq -r '.response' > schema-docs.md
|
||||
echo "Recent usage trends:"
|
||||
tail -5 usage.log
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [CLI Configuration](./cli/configuration.md) - Complete configuration guide
|
||||
- [Authentication](./cli/authentication.md) - Setup authentication
|
||||
- [Commands](./cli/commands.md) - Interactive commands reference
|
||||
- [Tutorials](./cli/tutorials.md) - Step-by-step automation guides
|
||||
@@ -0,0 +1,184 @@
|
||||
# Gemini CLI Companion Plugin: Interface Specification
|
||||
|
||||
> Last Updated: September 15, 2025
|
||||
|
||||
This document defines the contract for building a companion plugin to enable Gemini CLI's IDE mode. For VS Code, these features (native diffing, context awareness) are provided by the official extension ([marketplace](https://marketplace.visualstudio.com/items?itemName=Google.gemini-cli-vscode-ide-companion)). This specification is for contributors who wish to bring similar functionality to other editors like JetBrains IDEs, Sublime Text, etc.
|
||||
|
||||
## I. The Communication Interface
|
||||
|
||||
Gemini CLI and the IDE plugin communicate through a local communication channel.
|
||||
|
||||
### 1. Transport Layer: MCP over HTTP
|
||||
|
||||
The plugin **MUST** run a local HTTP server that implements the **Model Context Protocol (MCP)**.
|
||||
|
||||
- **Protocol:** The server must be a valid MCP server. We recommend using an existing MCP SDK for your language of choice if available.
|
||||
- **Endpoint:** The server should expose a single endpoint (e.g., `/mcp`) for all MCP communication.
|
||||
- **Port:** The server **MUST** listen on a dynamically assigned port (i.e., listen on port `0`).
|
||||
|
||||
### 2. Discovery Mechanism: The Port File
|
||||
|
||||
For Gemini CLI to connect, it needs to discover which IDE instance it's running in and what port your server is using. The plugin **MUST** facilitate this by creating a "discovery file."
|
||||
|
||||
- **How the CLI Finds the File:** The CLI determines the Process ID (PID) of the IDE it's running in by traversing the process tree. It then looks for a discovery file that contains this PID in its name.
|
||||
- **File Location:** The file must be created in a specific directory: `os.tmpdir()/gemini/ide/`. Your plugin must create this directory if it doesn't exist.
|
||||
- **File Naming Convention:** The filename is critical and **MUST** follow the pattern:
|
||||
`gemini-ide-server-${PID}-${PORT}.json`
|
||||
- `${PID}`: The process ID of the parent IDE process. Your plugin must determine this PID and include it in the filename.
|
||||
- `${PORT}`: The port your MCP server is listening on.
|
||||
- **File Content & Workspace Validation:** The file **MUST** contain a JSON object with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 12345,
|
||||
"workspacePath": "/path/to/project1:/path/to/project2",
|
||||
"authToken": "a-very-secret-token",
|
||||
"ideInfo": {
|
||||
"name": "vscode",
|
||||
"displayName": "VS Code"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `port` (number, required): The port of the MCP server.
|
||||
- `workspacePath` (string, required): A list of all open workspace root paths, delimited by the OS-specific path separator (`:` for Linux/macOS, `;` for Windows). The CLI uses this path to ensure it's running in the same project folder that's open in the IDE. If the CLI's current working directory is not a sub-directory of `workspacePath`, the connection will be rejected. Your plugin **MUST** provide the correct, absolute path(s) to the root of the open workspace(s).
|
||||
- `authToken` (string, required): A secret token for securing the connection. The CLI will include this token in an `Authorization: Bearer <token>` header on all requests.
|
||||
- `ideInfo` (object, required): Information about the IDE.
|
||||
- `name` (string, required): A short, lowercase identifier for the IDE (e.g., `vscode`, `jetbrains`).
|
||||
- `displayName` (string, required): A user-friendly name for the IDE (e.g., `VS Code`, `JetBrains IDE`).
|
||||
|
||||
- **Authentication:** To secure the connection, the plugin **MUST** generate a unique, secret token and include it in the discovery file. The CLI will then include this token in the `Authorization` header for all requests to the MCP server (e.g., `Authorization: Bearer a-very-secret-token`). Your server **MUST** validate this token on every request and reject any that are unauthorized.
|
||||
- **Tie-Breaking with Environment Variables (Recommended):** For the most reliable experience, your plugin **SHOULD** both create the discovery file and set the `GEMINI_CLI_IDE_SERVER_PORT` environment variable in the integrated terminal. The file serves as the primary discovery mechanism, but the environment variable is crucial for tie-breaking. If a user has multiple IDE windows open for the same workspace, the CLI uses the `GEMINI_CLI_IDE_SERVER_PORT` variable to identify and connect to the correct window's server.
|
||||
|
||||
## II. The Context Interface
|
||||
|
||||
To enable context awareness, the plugin **MAY** provide the CLI with real-time information about the user's activity in the IDE.
|
||||
|
||||
### `ide/contextUpdate` Notification
|
||||
|
||||
The plugin **MAY** send an `ide/contextUpdate` [notification](https://modelcontextprotocol.io/specification/2025-06-18/basic/index#notifications) to the CLI whenever the user's context changes.
|
||||
|
||||
- **Triggering Events:** This notification should be sent (with a recommended debounce of 50ms) when:
|
||||
- A file is opened, closed, or focused.
|
||||
- The user's cursor position or text selection changes in the active file.
|
||||
- **Payload (`IdeContext`):** The notification parameters **MUST** be an `IdeContext` object:
|
||||
|
||||
```typescript
|
||||
interface IdeContext {
|
||||
workspaceState?: {
|
||||
openFiles?: File[];
|
||||
isTrusted?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface File {
|
||||
// Absolute path to the file
|
||||
path: string;
|
||||
// Last focused Unix timestamp (for ordering)
|
||||
timestamp: number;
|
||||
// True if this is the currently focused file
|
||||
isActive?: boolean;
|
||||
cursor?: {
|
||||
// 1-based line number
|
||||
line: number;
|
||||
// 1-based character number
|
||||
character: number;
|
||||
};
|
||||
// The text currently selected by the user
|
||||
selectedText?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `openFiles` list should only include files that exist on disk. Virtual files (e.g., unsaved files without a path, editor settings pages) **MUST** be excluded.
|
||||
|
||||
### How the CLI Uses This Context
|
||||
|
||||
After receiving the `IdeContext` object, the CLI performs several normalization and truncation steps before sending the information to the model.
|
||||
|
||||
- **File Ordering:** The CLI uses the `timestamp` field to determine the most recently used files. It sorts the `openFiles` list based on this value. Therefore, your plugin **MUST** provide an accurate Unix timestamp for when a file was last focused.
|
||||
- **Active File:** The CLI considers only the most recent file (after sorting) to be the "active" file. It will ignore the `isActive` flag on all other files and clear their `cursor` and `selectedText` fields. Your plugin should focus on setting `isActive: true` and providing cursor/selection details only for the currently focused file.
|
||||
- **Truncation:** To manage token limits, the CLI truncates both the file list (to 10 files) and the `selectedText` (to 16KB).
|
||||
|
||||
While the CLI handles the final truncation, it is highly recommended that your plugin also limits the amount of context it sends.
|
||||
|
||||
## III. The Diffing Interface
|
||||
|
||||
To enable interactive code modifications, the plugin **MAY** expose a diffing interface. This allows the CLI to request that the IDE open a diff view, showing proposed changes to a file. The user can then review, edit, and ultimately accept or reject these changes directly within the IDE.
|
||||
|
||||
### `openDiff` Tool
|
||||
|
||||
The plugin **MUST** register an `openDiff` tool on its MCP server.
|
||||
|
||||
- **Description:** This tool instructs the IDE to open a modifiable diff view for a specific file.
|
||||
- **Request (`OpenDiffRequest`):** The tool is invoked via a `tools/call` request. The `arguments` field within the request's `params` **MUST** be an `OpenDiffRequest` object.
|
||||
|
||||
```typescript
|
||||
interface OpenDiffRequest {
|
||||
// The absolute path to the file to be diffed.
|
||||
filePath: string;
|
||||
// The proposed new content for the file.
|
||||
newContent: string;
|
||||
}
|
||||
```
|
||||
|
||||
- **Response (`CallToolResult`):** The tool **MUST** immediately return a `CallToolResult` to acknowledge the request and report whether the diff view was successfully opened.
|
||||
- On Success: If the diff view was opened successfully, the response **MUST** contain empty content (i.e., `content: []`).
|
||||
- On Failure: If an error prevented the diff view from opening, the response **MUST** have `isError: true` and include a `TextContent` block in the `content` array describing the error.
|
||||
|
||||
The actual outcome of the diff (acceptance or rejection) is communicated asynchronously via notifications.
|
||||
|
||||
### `closeDiff` Tool
|
||||
|
||||
The plugin **MUST** register a `closeDiff` tool on its MCP server.
|
||||
|
||||
- **Description:** This tool instructs the IDE to close an open diff view for a specific file.
|
||||
- **Request (`CloseDiffRequest`):** The tool is invoked via a `tools/call` request. The `arguments` field within the request's `params` **MUST** be an `CloseDiffRequest` object.
|
||||
|
||||
```typescript
|
||||
interface CloseDiffRequest {
|
||||
// The absolute path to the file whose diff view should be closed.
|
||||
filePath: string;
|
||||
}
|
||||
```
|
||||
|
||||
- **Response (`CallToolResult`):** The tool **MUST** return a `CallToolResult`.
|
||||
- On Success: If the diff view was closed successfully, the response **MUST** include a single **TextContent** block in the content array containing the file's final content before closing.
|
||||
- On Failure: If an error prevented the diff view from closing, the response **MUST** have `isError: true` and include a `TextContent` block in the `content` array describing the error.
|
||||
|
||||
### `ide/diffAccepted` Notification
|
||||
|
||||
When the user accepts the changes in a diff view (e.g., by clicking an "Apply" or "Save" button), the plugin **MUST** send an `ide/diffAccepted` notification to the CLI.
|
||||
|
||||
- **Payload:** The notification parameters **MUST** include the file path and the final content of the file. The content may differ from the original `newContent` if the user made manual edits in the diff view.
|
||||
|
||||
```typescript
|
||||
{
|
||||
// The absolute path to the file that was diffed.
|
||||
filePath: string;
|
||||
// The full content of the file after acceptance.
|
||||
content: string;
|
||||
}
|
||||
```
|
||||
|
||||
### `ide/diffRejected` Notification
|
||||
|
||||
When the user rejects the changes (e.g., by closing the diff view without accepting), the plugin **MUST** send an `ide/diffRejected` notification to the CLI.
|
||||
|
||||
- **Payload:** The notification parameters **MUST** include the file path of the rejected diff.
|
||||
|
||||
```typescript
|
||||
{
|
||||
// The absolute path to the file that was diffed.
|
||||
filePath: string;
|
||||
}
|
||||
```
|
||||
|
||||
## IV. The Lifecycle Interface
|
||||
|
||||
The plugin **MUST** manage its resources and the discovery file correctly based on the IDE's lifecycle.
|
||||
|
||||
- **On Activation (IDE startup/plugin enabled):**
|
||||
1. Start the MCP server.
|
||||
2. Create the discovery file.
|
||||
- **On Deactivation (IDE shutdown/plugin disabled):**
|
||||
1. Stop the MCP server.
|
||||
2. Delete the discovery file.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Gemini CLI can integrate with your IDE to provide a more seamless and context-aware experience. This integration allows the CLI to understand your workspace better and enables powerful features like native in-editor diffing.
|
||||
|
||||
Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions.
|
||||
Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions. To build support for other editors, see the [IDE Companion Extension Spec](./ide-companion-spec.md).
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ This documentation is organized into the following sections:
|
||||
- **[Checkpointing](./checkpointing.md):** Documentation for the checkpointing feature.
|
||||
- **[Extensions](./extension.md):** How to extend the CLI with new functionality.
|
||||
- **[IDE Integration](./ide-integration.md):** Connect the CLI to your editor.
|
||||
- **[IDE Companion Extension Spec](./ide-companion-spec.md):** Spec for building IDE companion extensions.
|
||||
- **[Telemetry](./telemetry.md):** Overview of telemetry in the CLI.
|
||||
- **[Trusted Folders](./trusted-folders.md):** An overview of the Trusted Folders security feature.
|
||||
- **Core Details:** Documentation for `packages/core`.
|
||||
- **[Core Introduction](./core/index.md):** Overview of the core component.
|
||||
- **[Tools API](./core/tools-api.md):** Information on how the core manages and exposes tools.
|
||||
|
||||
@@ -20,7 +20,7 @@ npm run test:e2e
|
||||
|
||||
## Running a specific set of tests
|
||||
|
||||
To run a subset of test files, you can use `npm run <integration test command> <file_name1> ....` where <integration test command> is either `test:e2e` or `test:integration*` and `<file_name>` is any of the `.test.js` files in the `integration-tests/` directory. For example, the following command runs `list_directory.test.js` and `write_file.test.js`:
|
||||
To run a subset of test files, you can use `npm run <integration test command> <file_name1> ....` where <integration test command> is either `test:e2e` or `test:integration*` and `<file_name>` is any of the `.test.js` files in the `integration-tests/` directory. For example, the following command runs `list_directory.test.js` and `write_file.test.js`:
|
||||
|
||||
```bash
|
||||
npm run test:e2e list_directory write_file
|
||||
|
||||
@@ -73,7 +73,7 @@ This is a fallback workflow to ensure that no issue gets missed by the triage pr
|
||||
|
||||
This workflow handles the process of packaging and publishing new versions of the Gemini CLI.
|
||||
|
||||
- **Workflow File**: `.github/workflows/release.yml`
|
||||
- **Workflow File**: `.github/workflows/release-manual.yml`
|
||||
- **When it runs**: On a daily schedule for "nightly" releases, and manually for official patch/minor releases.
|
||||
- **What it does**:
|
||||
- Automatically builds the project, bumps the version numbers, and publishes the packages to npm.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
graph LR
|
||||
%% --- Style Definitions ---
|
||||
classDef new fill:#98fb98,color:#000
|
||||
classDef changed fill:#add8e6,color:#000
|
||||
classDef unchanged fill:#f0f0f0,color:#000
|
||||
|
||||
%% --- Subgraphs ---
|
||||
subgraph "Context Providers"
|
||||
direction TB
|
||||
A["gemini.tsx"]
|
||||
B["AppContainer.tsx"]
|
||||
end
|
||||
|
||||
subgraph "Contexts"
|
||||
direction TB
|
||||
CtxSession["SessionContext"]
|
||||
CtxVim["VimModeContext"]
|
||||
CtxSettings["SettingsContext"]
|
||||
CtxApp["AppContext"]
|
||||
CtxConfig["ConfigContext"]
|
||||
CtxUIState["UIStateContext"]
|
||||
CtxUIActions["UIActionsContext"]
|
||||
end
|
||||
|
||||
subgraph "Component Consumers"
|
||||
direction TB
|
||||
ConsumerApp["App"]
|
||||
ConsumerAppContainer["AppContainer"]
|
||||
ConsumerAppHeader["AppHeader"]
|
||||
ConsumerDialogManager["DialogManager"]
|
||||
ConsumerHistoryItem["HistoryItemDisplay"]
|
||||
ConsumerComposer["Composer"]
|
||||
ConsumerMainContent["MainContent"]
|
||||
ConsumerNotifications["Notifications"]
|
||||
end
|
||||
|
||||
%% --- Provider -> Context Connections ---
|
||||
A -.-> CtxSession
|
||||
A -.-> CtxVim
|
||||
A -.-> CtxSettings
|
||||
|
||||
B -.-> CtxApp
|
||||
B -.-> CtxConfig
|
||||
B -.-> CtxUIState
|
||||
B -.-> CtxUIActions
|
||||
B -.-> CtxSettings
|
||||
|
||||
%% --- Context -> Consumer Connections ---
|
||||
CtxSession -.-> ConsumerAppContainer
|
||||
CtxSession -.-> ConsumerApp
|
||||
|
||||
CtxVim -.-> ConsumerAppContainer
|
||||
CtxVim -.-> ConsumerComposer
|
||||
CtxVim -.-> ConsumerApp
|
||||
|
||||
CtxSettings -.-> ConsumerAppContainer
|
||||
CtxSettings -.-> ConsumerAppHeader
|
||||
CtxSettings -.-> ConsumerDialogManager
|
||||
CtxSettings -.-> ConsumerApp
|
||||
|
||||
CtxApp -.-> ConsumerAppHeader
|
||||
CtxApp -.-> ConsumerNotifications
|
||||
|
||||
CtxConfig -.-> ConsumerAppHeader
|
||||
CtxConfig -.-> ConsumerHistoryItem
|
||||
CtxConfig -.-> ConsumerComposer
|
||||
CtxConfig -.-> ConsumerDialogManager
|
||||
|
||||
|
||||
|
||||
CtxUIState -.-> ConsumerApp
|
||||
CtxUIState -.-> ConsumerMainContent
|
||||
CtxUIState -.-> ConsumerComposer
|
||||
CtxUIState -.-> ConsumerDialogManager
|
||||
|
||||
CtxUIActions -.-> ConsumerComposer
|
||||
CtxUIActions -.-> ConsumerDialogManager
|
||||
|
||||
%% --- Apply Styles ---
|
||||
%% New Elements (Green)
|
||||
class B,CtxApp,CtxConfig,CtxUIState,CtxUIActions,ConsumerAppHeader,ConsumerDialogManager,ConsumerComposer,ConsumerMainContent,ConsumerNotifications new
|
||||
|
||||
%% Heavily Changed Elements (Blue)
|
||||
class A,ConsumerApp,ConsumerAppContainer,ConsumerHistoryItem changed
|
||||
|
||||
%% Mostly Unchanged Elements (Gray)
|
||||
class CtxSession,CtxVim,CtxSettings unchanged
|
||||
|
||||
%% --- Link Styles ---
|
||||
%% CtxSession (Red)
|
||||
linkStyle 0,8,9 stroke:#e57373,stroke-width:2px
|
||||
%% CtxVim (Orange)
|
||||
linkStyle 1,10,11,12 stroke:#ffb74d,stroke-width:2px
|
||||
%% CtxSettings (Yellow)
|
||||
linkStyle 2,7,13,14,15,16 stroke:#fff176,stroke-width:2px
|
||||
%% CtxApp (Green)
|
||||
linkStyle 3,17,18 stroke:#81c784,stroke-width:2px
|
||||
%% CtxConfig (Blue)
|
||||
linkStyle 4,19,20,21,22 stroke:#64b5f6,stroke-width:2px
|
||||
%% CtxUIState (Indigo)
|
||||
linkStyle 5,23,24,25,26 stroke:#7986cb,stroke-width:2px
|
||||
%% CtxUIActions (Violet)
|
||||
linkStyle 6,27,28 stroke:#ba68c8,stroke-width:2px
|
||||
@@ -0,0 +1,64 @@
|
||||
graph TD
|
||||
%% --- Style Definitions ---
|
||||
classDef new fill:#98fb98,color:#000
|
||||
classDef changed fill:#add8e6,color:#000
|
||||
classDef unchanged fill:#f0f0f0,color:#000
|
||||
classDef dispatcher fill:#f9e79f,color:#000,stroke:#333,stroke-width:1px
|
||||
classDef container fill:#f5f5f5,color:#000,stroke:#ccc
|
||||
|
||||
%% --- Component Tree ---
|
||||
subgraph "Entry Point"
|
||||
A["gemini.tsx"]
|
||||
end
|
||||
|
||||
subgraph "State & Logic Wrapper"
|
||||
B["AppContainer.tsx"]
|
||||
end
|
||||
|
||||
subgraph "Primary Layout"
|
||||
C["App.tsx"]
|
||||
end
|
||||
|
||||
A -.-> B
|
||||
B -.-> C
|
||||
|
||||
subgraph "UI Containers"
|
||||
direction LR
|
||||
C -.-> D["MainContent"]
|
||||
C -.-> G["Composer"]
|
||||
C -.-> F["DialogManager"]
|
||||
C -.-> E["Notifications"]
|
||||
end
|
||||
|
||||
subgraph "MainContent"
|
||||
direction TB
|
||||
D -.-> H["AppHeader"]
|
||||
D -.-> I["HistoryItemDisplay"]:::dispatcher
|
||||
D -.-> L["ShowMoreLines"]
|
||||
end
|
||||
|
||||
subgraph "Composer"
|
||||
direction TB
|
||||
G -.-> K_Prompt["InputPrompt"]
|
||||
G -.-> K_Footer["Footer"]
|
||||
end
|
||||
|
||||
subgraph "DialogManager"
|
||||
F -.-> J["Various Dialogs<br>(Auth, Theme, Settings, etc.)"]
|
||||
end
|
||||
|
||||
%% --- Apply Styles ---
|
||||
class B,D,E,F,G,H,J,K_Prompt,L new
|
||||
class A,C,I changed
|
||||
class K_Footer unchanged
|
||||
|
||||
%% --- Link Styles ---
|
||||
%% MainContent Branch (Blue)
|
||||
linkStyle 2,6,7,8 stroke:#64b5f6,stroke-width:2px
|
||||
%% Composer Branch (Green)
|
||||
linkStyle 3,9,10 stroke:#81c784,stroke-width:2px
|
||||
%% DialogManager Branch (Orange)
|
||||
linkStyle 4,11 stroke:#ffb74d,stroke-width:2px
|
||||
%% Notifications Branch (Violet)
|
||||
linkStyle 5 stroke:#ba68c8,stroke-width:2px
|
||||
|
||||
+20
-11
@@ -20,7 +20,7 @@ Your journey begins with a generous free tier, perfect for experimentation and l
|
||||
|
||||
Your free usage limits depend on your authorization type.
|
||||
|
||||
### Log in with Google (Gemini Code Assist for Individuals)
|
||||
### Log in with Google (Gemini Code Assist for individuals)
|
||||
|
||||
For users who authenticate by using their Google account to access Gemini Code Assist for individuals. This includes:
|
||||
|
||||
@@ -51,17 +51,26 @@ Learn more at [Vertex AI Express Mode Limits](https://cloud.google.com/vertex-ai
|
||||
|
||||
## Paid tier: Higher limits for a fixed cost
|
||||
|
||||
If you use up your initial number of requests, you can upgrade your plan to continue to benefit from Gemini CLI by using the [Standard or Enterprise editions of Gemini Code Assist](https://cloud.google.com/products/gemini/pricing) by signing up [here](https://goo.gle/set-up-gemini-code-assist). Quotas and pricing are based on a fixed price subscription with assigned license seats. For predictable costs, you can log in with Google. This includes:
|
||||
If you use up your initial number of requests, you can continue to benefit from Gemini CLI by upgrading to one of the following subscriptions:
|
||||
|
||||
- Standard:
|
||||
- 1500 model requests / user / day
|
||||
- 120 model requests / user / minute
|
||||
- Enterprise:
|
||||
- 2000 model requests / user / day
|
||||
- 120 model requests / user / minute
|
||||
- Model requests will be made across the Gemini model family as determined by Gemini CLI.
|
||||
- [Google AI Pro and AI Ultra](https://cloud.google.com/products/gemini/pricing) by signing up at [Set up Gemini Code Assist](https://goo.gle/set-up-gemini-code-assist). This is recommended for individual developers. Quotas and pricing are based on a fixed price subscription.
|
||||
|
||||
Learn more at [Gemini Code Assist Standard and Enterprise license Limits](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli).
|
||||
For predictable costs, you can log in with Google.
|
||||
|
||||
Learn more at [Gemini Code Assist Quotas and Limits](https://developers.google.com/gemini-code-assist/resources/quotas)
|
||||
|
||||
- [Purchase a Gemini Code Assist Subscription through Google Cloud ](https://cloud.google.com/gemini/docs/codeassist/overview) by signing up in the Google Cloud console. Learn more at [Set up Gemini Code Assist] (https://cloud.google.com/gemini/docs/discover/set-up-gemini) Quotas and pricing are based on a fixed price subscription with assigned license seats. For predictable costs, you can sign in with Google.
|
||||
|
||||
This includes:
|
||||
- Gemini Code Assist Standard edition:
|
||||
- 1500 model requests / user / day
|
||||
- 120 model requests / user / minute
|
||||
- Gemini Code Assist Enterprise edition:
|
||||
- 2000 model requests / user / day
|
||||
- 120 model requests / user / minute
|
||||
- Model requests will be made across the Gemini model family as determined by Gemini CLI.
|
||||
|
||||
[Learn more about Gemini Code Assist Standard and Enterprise license limits](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli).
|
||||
|
||||
## Pay As You Go
|
||||
|
||||
@@ -83,7 +92,7 @@ Learn more at [Gemini API Rate Limits](https://ai.google.dev/gemini-api/docs/rat
|
||||
|
||||
It’s important to highlight that when using an API key, you pay per token/call. This can be more expensive for many small calls with few tokens, but it's the only way to ensure your workflow isn't interrupted by quota limits.
|
||||
|
||||
## Google One and Ultra plans, Gemini for Workspace plans
|
||||
## Gemini for Workspace plans
|
||||
|
||||
These plans currently apply only to the use of Gemini web-based products provided by Google-based experiences (for example, the Gemini web app or the Flow video editor). These plans do not apply to the API usage which powers the Gemini CLI. Supporting these plans is under active consideration for future support.
|
||||
|
||||
|
||||
+258
-232
@@ -2,11 +2,18 @@
|
||||
|
||||
## Release Cadence and Tags
|
||||
|
||||
We will follow https://semver.org/ as closely as possible but will call out when or if we have to deviate from it. Our weekly releases will be minor version increments and any bug or hotfixes btween releases will go out as patch versions on the most recent release.
|
||||
We will follow https://semver.org/ as closely as possible but will call out when or if we have to deviate from it. Our weekly releases will be minor version increments and any bug or hotfixes between releases will go out as patch versions on the most recent release.
|
||||
|
||||
Each Tuesday ~2000 UTC new Stable and Preview releases will be cut. The promotion flow is:
|
||||
|
||||
- Code is commited to main and pushed each night to nightly
|
||||
- After no more than 1 week on main, code is promoted to the `preview` channel
|
||||
- After 1 week the most recent `preview` channel is promoted to `stable` cannel
|
||||
- Patch fixes will be produced against both `preview` and `stable` as needed, with the final 'patch' version number incrementing each time.
|
||||
|
||||
### Preview
|
||||
|
||||
New preview releases will be published each week at UTC 2359 on Tuesdays. These releases will not have been fully vetted and may contain regressions or other outstanding issues. Please help us test and install with `preview` tag.
|
||||
These releases will not have been fully vetted and may contain regressions or other outstanding issues. Please help us test and install with `preview` tag.
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@preview
|
||||
@@ -14,7 +21,7 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
### Stable
|
||||
|
||||
- New stable releases will be published each week at UTC 2000 on Tuesdays, this will be the full promotion of last week's release + any bug fixes and validations. Use `latest` tag.
|
||||
This will be the full promotion of last week's release + any bug fixes and validations. Use `latest` tag.
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@latest
|
||||
@@ -22,177 +29,227 @@ npm install -g @google/gemini-cli@latest
|
||||
|
||||
### Nightly
|
||||
|
||||
- New releases will be published each week at UTC 0000 each day, This will be all changes from the main branch as represted at time of release. It should be assumed there are pending validations and issues. Use `nightly` tag.
|
||||
- New releases will be published each day at UTC 0000. This will be all changes from the main branch as represented at time of release. It should be assumed there are pending validations and issues. Use `nightly` tag.
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@nightly
|
||||
```
|
||||
|
||||
# Release Process.
|
||||
## Weekly Release Promotion
|
||||
|
||||
Where `x.y.z` is the next version to be released. In most all cases for the weekly release this will be an increment on `y`, aka minor version update. Major version updates `x` will need broader coordination and communication. For patches `z` see below. When possible we will do our best to adher to https://semver.org/
|
||||
Each Tuesday, the on-call engineer will trigger the "Promote Release" workflow. This single action automates the entire weekly release process:
|
||||
|
||||
Our release cadence is new releases are sent to a preview channel for a week and then promoted to stable after a week. Version numbers will follow SemVer with weekly releases incrementing the minor version. Patches and bug fixes to both preview and stable releases will increment the patch version.
|
||||
1. **Promotes Preview to Stable:** The workflow identifies the latest `preview` release and promotes it to `stable`. This becomes the new `latest` version on npm.
|
||||
2. **Promotes Nightly to Preview:** The latest `nightly` release is then promoted to become the new `preview` version.
|
||||
3. **Prepares for next Nightly:** A pull request is automatically created and merged to bump the version in `main` in preparation for the next nightly release.
|
||||
|
||||
## Nightly Release
|
||||
This process ensures a consistent and reliable release cadence with minimal manual intervention.
|
||||
|
||||
Each night at UTC 0000 we will auto deploy a nightly release from `main`. This will be a version of the next production release, x.y.z, with the nightly tag.
|
||||
### Source of Truth for Versioning
|
||||
|
||||
## Create Preview Release
|
||||
To ensure the highest reliability, the release promotion process uses the **NPM registry as the single source of truth** for determining the current version of each release channel (`stable`, `preview`, and `nightly`).
|
||||
|
||||
Each Tuesday at UTC 2359 we will auto deploy a preview release of the next production release x.y.z.
|
||||
1. **Fetch from NPM:** The workflow begins by querying NPM's `dist-tags` (`latest`, `preview`, `nightly`) to get the exact version strings for the packages currently available to users.
|
||||
2. **Cross-Check for Integrity:** For each version retrieved from NPM, the workflow performs a critical integrity check:
|
||||
- It verifies that a corresponding **git tag** exists in the repository.
|
||||
- It verifies that a corresponding **GitHub Release** has been created.
|
||||
3. **Halt on Discrepancy:** If either the git tag or the GitHub Release is missing for a version listed on NPM, the workflow will immediately fail. This strict check prevents promotions from a broken or incomplete previous release and alerts the on-call engineer to a release state inconsistency that must be manually resolved.
|
||||
4. **Calculate Next Version:** Only after these checks pass does the workflow proceed to calculate the next semantic version based on the trusted version numbers retrieved from NPM.
|
||||
|
||||
- This will happen as a scheduled instance of the ‘release’ action. It will be cut off of Main.
|
||||
- This will create a branch `release/vx.y.z-preview.n`
|
||||
- We will run evals and smoke testing against this branch and the npm package. For now this should be manual smoke testing, we don't have a dedicated matrix or specific detailed process. There is work coming soon to make this more formalized and automatic see https://github.com/google-gemini/gemini-cli/issues/3788
|
||||
- Users installing `@preview` will get this release as well
|
||||
This NPM-first approach, backed by integrity checks, makes the release process highly robust and prevents the kinds of versioning discrepancies that can arise from relying solely on git history or API outputs.
|
||||
|
||||
## Promote Stable Release
|
||||
## Manual Releases
|
||||
|
||||
After one week (On the following Tuesday) with all signals a go, we will manually release at 2000 UTC via the current on-call person.
|
||||
For situations requiring a release outside of the regular nightly and weekly promotion schedule, and NOT already covered by patching process, you can use the `Release: Manual` workflow. This workflow provides a direct way to publish a specific version from any branch, tag, or commit SHA.
|
||||
|
||||
- The release action will be used with the source branch as `release/vx.y.z-preview.n`
|
||||
- The version will be x.y.z
|
||||
- The releaser will create and merge a pr into main with the version changes.
|
||||
- Smoke tests and manual validation will be run. For now this should be manual smoke testing, we don't have a dedicated matrix or specific detailed process. There is work coming soon to make this more formalized and automatic see https://github.com/google-gemini/gemini-cli/issues/3788
|
||||
|
||||
## Patching Releases
|
||||
|
||||
If a critical bug needs to be fixed before the next scheduled release, follow this process to create a patch.
|
||||
|
||||
### 1. Create a Hotfix Branch
|
||||
|
||||
First, create a new branch for your fix. The source for this branch depends on whether you are patching a stable or a preview release.
|
||||
|
||||
- **For a stable release patch:**
|
||||
Create a branch from the Git tag of the version you need to patch. Tag names are formatted as `vx.y.z`.
|
||||
|
||||
```bash
|
||||
# Example: Create a hotfix branch for v0.2.0
|
||||
git checkout v0.2.0 -b hotfix/issue-123-fix-for-v0.2.0
|
||||
```
|
||||
|
||||
- **For a preview release patch:**
|
||||
Create a branch from the existing preview release branch, which is formatted as `release/vx.y.z-preview.n`.
|
||||
|
||||
```bash
|
||||
# Example: Create a hotfix branch for a preview release
|
||||
git checkout release/v0.2.0-preview.0 && git checkout -b hotfix/issue-456-fix-for-preview
|
||||
```
|
||||
|
||||
### 2. Implement the Fix
|
||||
|
||||
In your new hotfix branch, either create a new commit with the fix or cherry-pick an existing commit from the `main` branch. Merge your changes into the source of the hotfix branch (ex. https://github.com/google-gemini/gemini-cli/pull/6850).
|
||||
|
||||
### 3. Perform the Release
|
||||
|
||||
Follow the manual release process using the "Release" GitHub Actions workflow.
|
||||
|
||||
- **Version**: For stable patches, increment the patch version (e.g., `v0.2.0` -> `v0.2.1`). For preview patches, increment the preview number (e.g., `v0.2.0-preview.0` -> `v0.2.0-preview.1`).
|
||||
- **Ref**: Use your source branch as the reference (ex. `release/v0.2.0-preview.0`)
|
||||
|
||||

|
||||
|
||||
### 4. Update Versions
|
||||
|
||||
After the hotfix is released, merge the changes back to the appropriate branch.
|
||||
|
||||
- **For a stable release hotfix:**
|
||||
Open a pull request to merge the release branch (e.g., `release/0.2.1`) back into `main`. This keeps the version number in `main` up to date.
|
||||
|
||||
- **For a preview release hotfix:**
|
||||
Open a pull request to merge the new preview release branch (e.g., `release/v0.2.0-preview.1`) back into the existing preview release branch (`release/v0.2.0-preview.0`) (ex. https://github.com/google-gemini/gemini-cli/pull/6868)
|
||||
|
||||
## Release Schedule
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Date
|
||||
</td>
|
||||
<td>Stable UTC 2000
|
||||
</td>
|
||||
<td>Preview UTC 2359
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aug 19th, 2025
|
||||
</td>
|
||||
<td>N/A
|
||||
</td>
|
||||
<td>0.2.0-preview.0
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aug 26th, 2025
|
||||
</td>
|
||||
<td>0.2.0
|
||||
</td>
|
||||
<td>0.3.0-preview.0
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sep 2nd, 2025
|
||||
</td>
|
||||
<td>0.3.0
|
||||
</td>
|
||||
<td>0.4.0-preview.0
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sep 9th, 2025
|
||||
</td>
|
||||
<td>0.4.0
|
||||
</td>
|
||||
<td>0.5.0-preview.0
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sep 16th, 2025
|
||||
</td>
|
||||
<td>0.5.0
|
||||
</td>
|
||||
<td>0.6.0-preview.0
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sep 23rd, 2025
|
||||
</td>
|
||||
<td>0.6.0
|
||||
</td>
|
||||
<td>0.7.0-preview.0
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## How To Release
|
||||
|
||||
Releases are managed through the [release.yml](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) GitHub Actions workflow. To perform a manual release for a patch or hotfix:
|
||||
### How to Create a Manual Release
|
||||
|
||||
1. Navigate to the **Actions** tab of the repository.
|
||||
2. Select the **Release** workflow from the list.
|
||||
2. Select the **Release: Manual** workflow from the list.
|
||||
3. Click the **Run workflow** dropdown button.
|
||||
4. Fill in the required inputs:
|
||||
- **Version**: The exact version to release (e.g., `v0.2.1`).
|
||||
- **Ref**: The branch or commit SHA to release from (defaults to `main`).
|
||||
- **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release.
|
||||
- **Version**: The exact version to release (e.g., `v0.6.1`). This must be a valid semantic version with a `v` prefix.
|
||||
- **Ref**: The branch, tag, or full commit SHA to release from.
|
||||
- **NPM Channel**: The npm channel to publish to. The options are `preview`, `nightly`, `latest` (for stable releases), and `dev`. The default is `dev`.
|
||||
- **Dry Run**: Leave as `true` to run all steps without publishing, or set to `false` to perform a live release.
|
||||
- **Force Skip Tests**: Set to `true` to skip the test suite. This is not recommended for production releases.
|
||||
- **Skip GitHub Release**: Set to `true` to skip creating a GitHub release and create an npm release only.
|
||||
5. Click **Run workflow**.
|
||||
|
||||
### TLDR
|
||||
The workflow will then proceed to test (if not skipped), build, and publish the release. If the workflow fails during a non-dry run, it will automatically create a GitHub issue with the failure details.
|
||||
|
||||
Each release, wether automated or manual performs the following steps:
|
||||
## Rollback/Rollforward
|
||||
|
||||
1. Checks out the latest code from the `main` branch.
|
||||
1. Installs all dependencies.
|
||||
1. Runs the full suite of `preflight` checks and integration tests.
|
||||
1. If all tests succeed, it calculates the next version number based on the inputs.
|
||||
1. It creates a branch name `release/${VERSION}`.
|
||||
1. It creates a tag name `v${VERSION}`.
|
||||
1. It then builds and publishes the packages to npm with the provided version number.
|
||||
1. Finally, it creates a GitHub Release for the version.
|
||||
In the event that a release has a critical regression, you can quickly roll back to a previous stable version or roll forward to a new patch by changing the npm `dist-tag`. The `Release: Change Tags` workflow provides a safe and controlled way to do this.
|
||||
|
||||
### Failure Handling
|
||||
This is the preferred method for both rollbacks and rollforwards, as it does not require a full release cycle.
|
||||
|
||||
If any step in the workflow fails, it will automatically create a new issue in the repository with the labels `bug` and `release-failure`. The issue will contain a link to the failed workflow run for easy debugging.
|
||||
### How to Change a Release Tag
|
||||
|
||||
1. Navigate to the **Actions** tab of the repository.
|
||||
2. Select the **Release: Change Tags** workflow from the list.
|
||||
3. Click the **Run workflow** dropdown button.
|
||||
4. Fill in the required inputs:
|
||||
- **Version**: The existing package version that you want to point the tag to (e.g., `0.5.0-preview-2`). This version **must** already be published to the npm registry.
|
||||
- **Channel**: The npm `dist-tag` to apply (e.g., `preview`, `stable`).
|
||||
- **Dry Run**: Leave as `true` to log the action without making changes, or set to `false` to perform the live tag change.
|
||||
5. Click **Run workflow**.
|
||||
|
||||
The workflow will then run `npm dist-tag add` for both the `@google/gemini-cli` and `@google/gemini-cli-core` packages, pointing the specified channel to the specified version.
|
||||
|
||||
## Patching
|
||||
|
||||
If a critical bug that is already fixed on `main` needs to be patched on a `stable` or `preview` release, the process is now highly automated.
|
||||
|
||||
### How to Patch
|
||||
|
||||
#### 1. Create the Patch Pull Request
|
||||
|
||||
There are two ways to create a patch pull request:
|
||||
|
||||
**Option A: From a GitHub Comment (Recommended)**
|
||||
|
||||
After a pull request containing the fix has been merged, a maintainer can add a comment on that same PR with the following format:
|
||||
|
||||
`/patch [channel]`
|
||||
|
||||
- **channel** (optional):
|
||||
- _no channel_ - patches both stable and preview channels (default, recommended for most fixes)
|
||||
- `both` - patches both stable and preview channels (same as default)
|
||||
- `stable` - patches only the stable channel
|
||||
- `preview` - patches only the preview channel
|
||||
|
||||
Examples:
|
||||
|
||||
- `/patch` (patches both stable and preview - default)
|
||||
- `/patch both` (patches both stable and preview - explicit)
|
||||
- `/patch stable` (patches only stable)
|
||||
- `/patch preview` (patches only preview)
|
||||
|
||||
The `Release: Patch from Comment` workflow will automatically find the merge commit SHA and trigger the `Release: Patch (1) Create PR` workflow. If the PR is not yet merged, it will post a comment indicating the failure.
|
||||
|
||||
**Option B: Manually Triggering the Workflow**
|
||||
|
||||
Navigate to the **Actions** tab and run the **Release: Patch (1) Create PR** workflow.
|
||||
|
||||
- **Commit**: The full SHA of the commit on `main` that you want to cherry-pick.
|
||||
- **Channel**: The channel you want to patch (`stable` or `preview`).
|
||||
|
||||
This workflow will automatically:
|
||||
|
||||
1. Find the latest release tag for the channel.
|
||||
2. Create a release branch from that tag if one doesn't exist (e.g., `release/v0.5.1`).
|
||||
3. Create a new hotfix branch from the release branch.
|
||||
4. Cherry-pick your specified commit into the hotfix branch.
|
||||
5. Create a pull request from the hotfix branch back to the release branch.
|
||||
|
||||
#### 2. Review and Merge
|
||||
|
||||
Review the automatically created pull request(s) to ensure the cherry-pick was successful and the changes are correct. Once approved, merge the pull request.
|
||||
|
||||
**Security Note:** The `release/*` branches are protected by branch protection rules. A pull request to one of these branches requires at least one review from a code owner before it can be merged. This ensures that no unauthorized code is released.
|
||||
|
||||
#### 2.5. Adding Multiple Commits to a Hotfix (Advanced)
|
||||
|
||||
If you need to include multiple fixes in a single patch release, you can add additional commits to the hotfix branch after the initial patch PR has been created:
|
||||
|
||||
1. **Start with the primary fix**: Use `/patch` (or `/patch both`) on the most important PR to create the initial hotfix branch and PR.
|
||||
|
||||
2. **Checkout the hotfix branch locally**:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout hotfix/v0.5.1/stable/cherry-pick-abc1234 # Use the actual branch name from the PR
|
||||
```
|
||||
|
||||
3. **Cherry-pick additional commits**:
|
||||
|
||||
```bash
|
||||
git cherry-pick <commit-sha-1>
|
||||
git cherry-pick <commit-sha-2>
|
||||
# Add as many commits as needed
|
||||
```
|
||||
|
||||
4. **Push the updated branch**:
|
||||
|
||||
```bash
|
||||
git push origin hotfix/v0.5.1/stable/cherry-pick-abc1234
|
||||
```
|
||||
|
||||
5. **Test and review**: The existing patch PR will automatically update with your additional commits. Test thoroughly since you're now releasing multiple changes together.
|
||||
|
||||
6. **Update the PR description**: Consider updating the PR title and description to reflect that it includes multiple fixes.
|
||||
|
||||
This approach allows you to group related fixes into a single patch release while maintaining full control over what gets included and how conflicts are resolved.
|
||||
|
||||
#### 3. Automatic Release
|
||||
|
||||
Upon merging the pull request, the `Release: Patch (2) Trigger` workflow is automatically triggered. It will then start the `Release: Patch (3) Release` workflow, which will:
|
||||
|
||||
1. Build and test the patched code.
|
||||
2. Publish the new patch version to npm.
|
||||
3. Create a new GitHub release with the patch notes.
|
||||
|
||||
This fully automated process ensures that patches are created and released consistently and reliably.
|
||||
|
||||
#### Troubleshooting: Older Branch Workflows
|
||||
|
||||
**Issue**: If the patch trigger workflow fails with errors like "Resource not accessible by integration" or references to non-existent workflow files (e.g., `patch-release.yml`), this indicates the hotfix branch contains an outdated version of the workflow files.
|
||||
|
||||
**Root Cause**: When a PR is merged, GitHub Actions runs the workflow definition from the **source branch** (the hotfix branch), not from the target branch (the release branch). If the hotfix branch was created from an older release branch that predates workflow improvements, it will use the old workflow logic.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
**Option 1: Manual Trigger (Quick Fix)**
|
||||
Manually trigger the updated workflow from the branch with the latest workflow code:
|
||||
|
||||
```bash
|
||||
# For a preview channel patch with tests skipped
|
||||
gh workflow run release-patch-2-trigger.yml --ref <branch-with-updated-workflow> \
|
||||
--field ref="hotfix/v0.6.0-preview.2/preview/cherry-pick-abc1234" \
|
||||
--field workflow_ref=<branch-with-updated-workflow> \
|
||||
--field dry_run=false \
|
||||
--field force_skip_tests=true
|
||||
|
||||
# For a stable channel patch
|
||||
gh workflow run release-patch-2-trigger.yml --ref <branch-with-updated-workflow> \
|
||||
--field ref="hotfix/v0.5.1/stable/cherry-pick-abc1234" \
|
||||
--field workflow_ref=<branch-with-updated-workflow> \
|
||||
--field dry_run=false \
|
||||
--field force_skip_tests=false
|
||||
|
||||
# Example using main branch (most common case)
|
||||
gh workflow run release-patch-2-trigger.yml --ref main \
|
||||
--field ref="hotfix/v0.6.0-preview.2/preview/cherry-pick-abc1234" \
|
||||
--field workflow_ref=main \
|
||||
--field dry_run=false \
|
||||
--field force_skip_tests=true
|
||||
```
|
||||
|
||||
**Note**: Replace `<branch-with-updated-workflow>` with the branch containing the latest workflow improvements (usually `main`, but could be a feature branch if testing updates).
|
||||
|
||||
**Option 2: Update the Hotfix Branch**
|
||||
Merge the latest main branch into your hotfix branch to get the updated workflows:
|
||||
|
||||
```bash
|
||||
git checkout hotfix/v0.6.0-preview.2/preview/cherry-pick-abc1234
|
||||
git merge main
|
||||
git push
|
||||
```
|
||||
|
||||
Then close and reopen the PR to retrigger the workflow with the updated version.
|
||||
|
||||
**Option 3: Direct Release Trigger**
|
||||
Skip the trigger workflow entirely and directly run the release workflow:
|
||||
|
||||
```bash
|
||||
# Replace channel and release_ref with appropriate values
|
||||
gh workflow run release-patch-3-release.yml --ref main \
|
||||
--field type="preview" \
|
||||
--field dry_run=false \
|
||||
--field force_skip_tests=true \
|
||||
--field release_ref="release/v0.6.0-preview.2"
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
@@ -211,7 +268,7 @@ After pushing a new release smoke testing should be performed to ensure that the
|
||||
|
||||
If you need to test the release process without actually publishing to NPM or creating a public GitHub release, you can trigger the workflow manually from the GitHub UI.
|
||||
|
||||
1. Go to the [Actions tab](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) of the repository.
|
||||
1. Go to the [Actions tab](https://github.com/google-gemini/gemini-cli/actions/workflows/release-manual.yml) of the repository.
|
||||
2. Click on the "Run workflow" dropdown.
|
||||
3. Leave the `dry_run` option checked (`true`).
|
||||
4. Click the "Run workflow" button.
|
||||
@@ -239,105 +296,74 @@ By performing a dry run, you can be confident that your changes to the packaging
|
||||
|
||||
## Release Deep Dive
|
||||
|
||||
The main goal of the release process is to take the source code from the packages/ directory, build it, and assemble a
|
||||
clean, self-contained package in a temporary `bundle` directory at the root of the project. This `bundle` directory is what
|
||||
actually gets published to NPM.
|
||||
The release process creates two distinct types of artifacts for different distribution channels: standard packages for the NPM registry and a single, self-contained executable for GitHub Releases.
|
||||
|
||||
Here are the key stages:
|
||||
|
||||
Stage 1: Pre-Release Sanity Checks and Versioning
|
||||
**Stage 1: Pre-Release Sanity Checks and Versioning**
|
||||
|
||||
- What happens: Before any files are moved, the process ensures the project is in a good state. This involves running tests,
|
||||
linting, and type-checking (npm run preflight). The version number in the root package.json and packages/cli/package.json
|
||||
is updated to the new release version.
|
||||
- Why: This guarantees that only high-quality, working code is released. Versioning is the first step to signify a new
|
||||
release.
|
||||
- **What happens:** Before any files are moved, the process ensures the project is in a good state. This involves running tests, linting, and type-checking (`npm run preflight`). The version number in the root `package.json` and `packages/cli/package.json` is updated to the new release version.
|
||||
|
||||
Stage 2: Building the Source Code
|
||||
**Stage 2: Building the Source Code for NPM**
|
||||
|
||||
- What happens: The TypeScript source code in packages/core/src and packages/cli/src is compiled into JavaScript.
|
||||
- File movement:
|
||||
- packages/core/src/\*_/_.ts -> compiled to -> packages/core/dist/
|
||||
- packages/cli/src/\*_/_.ts -> compiled to -> packages/cli/dist/
|
||||
- Why: The TypeScript code written during development needs to be converted into plain JavaScript that can be run by
|
||||
Node.js. The core package is built first as the cli package depends on it.
|
||||
- **What happens:** The TypeScript source code in `packages/core/src` and `packages/cli/src` is compiled into standard JavaScript.
|
||||
- **File movement:**
|
||||
- `packages/core/src/**/*.ts` -> compiled to -> `packages/core/dist/`
|
||||
- `packages/cli/src/**/*.ts` -> compiled to -> `packages/cli/dist/`
|
||||
- **Why:** The TypeScript code written during development needs to be converted into plain JavaScript that can be run by Node.js. The `core` package is built first as the `cli` package depends on it.
|
||||
|
||||
Stage 3: Assembling the Final Publishable Package
|
||||
**Stage 3: Publishing Standard Packages to NPM**
|
||||
|
||||
This is the most critical stage where files are moved and transformed into their final state for publishing. A temporary
|
||||
`bundle` folder is created at the project root to house the final package contents.
|
||||
- **What happens:** The `npm publish` command is run for the `@google/gemini-cli-core` and `@google/gemini-cli` packages.
|
||||
- **Why:** This publishes them as standard Node.js packages. Users installing via `npm install -g @google/gemini-cli` will download these packages, and `npm` will handle installing the `@google/gemini-cli-core` dependency automatically. The code in these packages is not bundled into a single file.
|
||||
|
||||
1. The `package.json` is Transformed:
|
||||
- What happens: The package.json from packages/cli/ is read, modified, and written into the root `bundle`/ directory.
|
||||
- File movement: packages/cli/package.json -> (in-memory transformation) -> `bundle`/package.json
|
||||
- Why: The final package.json must be different from the one used in development. Key changes include:
|
||||
- Removing devDependencies.
|
||||
- Removing workspace-specific "dependencies": { "@gemini-cli/core": "workspace:\*" } and ensuring the core code is
|
||||
bundled directly into the final JavaScript file.
|
||||
- Ensuring the bin, main, and files fields point to the correct locations within the final package structure.
|
||||
**Stage 4: Assembling and Creating the GitHub Release Asset**
|
||||
|
||||
2. The JavaScript Bundle is Created:
|
||||
- What happens: The built JavaScript from both packages/core/dist and packages/cli/dist are bundled into a single,
|
||||
executable JavaScript file.
|
||||
- File movement: packages/cli/dist/index.js + packages/core/dist/index.js -> (bundled by esbuild) -> `bundle`/gemini.js (or a
|
||||
similar name).
|
||||
- Why: This creates a single, optimized file that contains all the necessary application code. It simplifies the package
|
||||
by removing the need for the core package to be a separate dependency on NPM, as its code is now included directly.
|
||||
This stage happens _after_ the NPM publish and creates the single-file executable that enables `npx` usage directly from the GitHub repository.
|
||||
|
||||
3. Static and Supporting Files are Copied:
|
||||
- What happens: Essential files that are not part of the source code but are required for the package to work correctly
|
||||
or be well-described are copied into the `bundle` directory.
|
||||
- File movement:
|
||||
- README.md -> `bundle`/README.md
|
||||
- LICENSE -> `bundle`/LICENSE
|
||||
- packages/cli/src/utils/\*.sb (sandbox profiles) -> `bundle`/
|
||||
- Why:
|
||||
- The README.md and LICENSE are standard files that should be included in any NPM package.
|
||||
- The sandbox profiles (.sb files) are critical runtime assets required for the CLI's sandboxing feature to
|
||||
function. They must be located next to the final executable.
|
||||
1. **The JavaScript Bundle is Created:**
|
||||
- **What happens:** The built JavaScript from both `packages/core/dist` and `packages/cli/dist`, along with all third-party JavaScript dependencies, are bundled by `esbuild` into a single, executable JavaScript file (e.g., `gemini.js`). The `node-pty` library is excluded from this bundle as it contains native binaries.
|
||||
- **Why:** This creates a single, optimized file that contains all the necessary application code. It simplifies execution for users who want to run the CLI without a full `npm install`, as all dependencies (including the `core` package) are included directly.
|
||||
|
||||
Stage 4: Publishing to NPM
|
||||
2. **The `bundle` Directory is Assembled:**
|
||||
- **What happens:** A temporary `bundle` folder is created at the project root. The single `gemini.js` executable is placed inside it, along with other essential files.
|
||||
- **File movement:**
|
||||
- `gemini.js` (from esbuild) -> `bundle/gemini.js`
|
||||
- `README.md` -> `bundle/README.md`
|
||||
- `LICENSE` -> `bundle/LICENSE`
|
||||
- `packages/cli/src/utils/*.sb` (sandbox profiles) -> `bundle/`
|
||||
- **Why:** This creates a clean, self-contained directory with everything needed to run the CLI and understand its license and usage.
|
||||
|
||||
- What happens: The npm publish command is run from inside the root `bundle` directory.
|
||||
- Why: By running npm publish from within the `bundle` directory, only the files we carefully assembled in Stage 3 are uploaded
|
||||
to the NPM registry. This prevents any source code, test files, or development configurations from being accidentally
|
||||
published, resulting in a clean and minimal package for users.
|
||||
3. **The GitHub Release is Created:**
|
||||
- **What happens:** The contents of the `bundle` directory, including the `gemini.js` executable, are attached as assets to a new GitHub Release.
|
||||
- **Why:** This makes the single-file version of the CLI available for direct download and enables the `npx https://github.com/google-gemini/gemini-cli` command, which downloads and runs this specific bundled asset.
|
||||
|
||||
Summary of File Flow
|
||||
**Summary of Artifacts**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Source Files"
|
||||
A["packages/core/src/*.ts<br/>packages/cli/src/*.ts"]
|
||||
B["packages/cli/package.json"]
|
||||
C["README.md<br/>LICENSE<br/>packages/cli/src/utils/*.sb"]
|
||||
end
|
||||
- **NPM:** Publishes standard, un-bundled Node.js packages. The primary artifact is the code in `packages/cli/dist`, which depends on `@google/gemini-cli-core`.
|
||||
- **GitHub Release:** Publishes a single, bundled `gemini.js` file that contains all dependencies, for easy execution via `npx`.
|
||||
|
||||
subgraph "Process"
|
||||
D(Build)
|
||||
E(Transform)
|
||||
F(Assemble)
|
||||
G(Publish)
|
||||
end
|
||||
This dual-artifact process ensures that both traditional `npm` users and those who prefer the convenience of `npx` have an optimized experience.
|
||||
|
||||
subgraph "Artifacts"
|
||||
H["Bundled JS"]
|
||||
I["Final package.json"]
|
||||
J["bundle/"]
|
||||
end
|
||||
## Notifications
|
||||
|
||||
subgraph "Destination"
|
||||
K["NPM Registry"]
|
||||
end
|
||||
Failing release workflows will automatically create an issue with the label
|
||||
`release-failure`.
|
||||
|
||||
A --> D --> H
|
||||
B --> E --> I
|
||||
C --> F
|
||||
H --> F
|
||||
I --> F
|
||||
F --> J
|
||||
J --> G --> K
|
||||
```
|
||||
A notification will be posted to the maintainer's chat channel when issues with
|
||||
this type are created.
|
||||
|
||||
This process ensures that the final published artifact is a purpose-built, clean, and efficient representation of the
|
||||
project, rather than a direct copy of the development workspace.
|
||||
### Modifying chat notifications
|
||||
|
||||
Notifications use [GitHub for Google Chat](https://workspace.google.com/marketplace/app/github_for_google_chat/536184076190). To modify the notifications, use `/github-settings` within the chat space.
|
||||
|
||||
> [!WARNING]
|
||||
> The following instructions describe a fragile workaround that depends on the internal structure of the chat application's UI. It is likely to break with future updates.
|
||||
|
||||
The list of available labels is not currently populated correctly. If you want to add a label that does not appear alphabetically in the first 30 labels in the repo, you must use your browser's developer tools to manually modify the UI:
|
||||
|
||||
1. Open your browser's developer tools (e.g., Chrome DevTools).
|
||||
2. In the `/github-settings` dialog, inspect the list of labels.
|
||||
3. Locate one of the `<li>` elements representing a label.
|
||||
4. In the HTML, modify the `data-option-value` attribute of that `<li>` element to the desired label name (e.g., `release-failure`).
|
||||
5. Click on your modified label in the UI to select it, then save your settings.
|
||||
|
||||
+4
-2
@@ -55,7 +55,9 @@ gemini -p "run the test suite"
|
||||
|
||||
# Configure in settings.json
|
||||
{
|
||||
"sandbox": "docker"
|
||||
"tools": {
|
||||
"sandbox": "docker"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -65,7 +67,7 @@ gemini -p "run the test suite"
|
||||
|
||||
1. **Command flag**: `-s` or `--sandbox`
|
||||
2. **Environment variable**: `GEMINI_SANDBOX=true|docker|podman|sandbox-exec`
|
||||
3. **Settings file**: `"sandbox": true` in `settings.json`
|
||||
3. **Settings file**: `"sandbox": true` in the `tools` object of your `settings.json` file (e.g., `{"tools": {"sandbox": true}}`).
|
||||
|
||||
### macOS Seatbelt profiles
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
[
|
||||
{
|
||||
"label": "Overview",
|
||||
"items": [
|
||||
{ "label": "Welcome", "slug": "docs" },
|
||||
{ "label": "Execution and Deployment", "slug": "docs/deployment" },
|
||||
{ "label": "Architecture Overview", "slug": "docs/architecture" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "CLI",
|
||||
"items": [
|
||||
{ "label": "Introduction", "slug": "docs/cli" },
|
||||
{ "label": "Authentication", "slug": "docs/cli/authentication" },
|
||||
{ "label": "Commands", "slug": "docs/cli/commands" },
|
||||
{ "label": "Configuration", "slug": "docs/cli/configuration" },
|
||||
{ "label": "Checkpointing", "slug": "docs/checkpointing" },
|
||||
{ "label": "Enterprise", "slug": "docs/cli/enterprise" },
|
||||
{ "label": "Extensions", "slug": "docs/extension" },
|
||||
{ "label": "Headless Mode", "slug": "docs/headless" },
|
||||
{ "label": "IDE Integration", "slug": "docs/ide-integration" },
|
||||
{
|
||||
"label": "IDE Companion Spec",
|
||||
"slug": "docs/ide-companion-spec"
|
||||
},
|
||||
{ "label": "Telemetry", "slug": "docs/telemetry" },
|
||||
{ "label": "Themes", "slug": "docs/cli/themes" },
|
||||
{ "label": "Token Caching", "slug": "docs/cli/token-caching" },
|
||||
{ "label": "Trusted Folders", "slug": "docs/trusted-folders" },
|
||||
{ "label": "Tutorials", "slug": "docs/cli/tutorials" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Core",
|
||||
"items": [
|
||||
{ "label": "Introduction", "slug": "docs/core" },
|
||||
{ "label": "Tools API", "slug": "docs/core/tools-api" },
|
||||
{ "label": "Memory Import Processor", "slug": "docs/core/memport" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Tools",
|
||||
"items": [
|
||||
{ "label": "Overview", "slug": "docs/tools" },
|
||||
{ "label": "File System", "slug": "docs/tools/file-system" },
|
||||
{ "label": "Multi-File Read", "slug": "docs/tools/multi-file" },
|
||||
{ "label": "Shell", "slug": "docs/tools/shell" },
|
||||
{ "label": "Web Fetch", "slug": "docs/tools/web-fetch" },
|
||||
{ "label": "Web Search", "slug": "docs/tools/web-search" },
|
||||
{ "label": "Memory", "slug": "docs/tools/memory" },
|
||||
{ "label": "MCP Servers", "slug": "docs/tools/mcp-server" },
|
||||
{ "label": "Sandboxing", "slug": "docs/sandbox" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Development",
|
||||
"items": [
|
||||
{ "label": "NPM", "slug": "docs/npm" },
|
||||
{ "label": "Releases", "slug": "docs/releases" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Support",
|
||||
"items": [
|
||||
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
|
||||
{ "label": "Terms of Service", "slug": "docs/tos-privacy" }
|
||||
]
|
||||
}
|
||||
]
|
||||
+218
-132
@@ -1,166 +1,216 @@
|
||||
# Gemini CLI Observability Guide
|
||||
# Observability with OpenTelemetry
|
||||
|
||||
Telemetry provides data about Gemini CLI's performance, health, and usage. By enabling it, you can monitor operations, debug issues, and optimize tool usage through traces, metrics, and structured logs.
|
||||
Learn how to enable and setup OpenTelemetry for Gemini CLI.
|
||||
|
||||
Gemini CLI's telemetry system is built on the **[OpenTelemetry] (OTEL)** standard, allowing you to send data to any compatible backend.
|
||||
- [Observability with OpenTelemetry](#observability-with-opentelemetry)
|
||||
- [Key Benefits](#key-benefits)
|
||||
- [OpenTelemetry Integration](#opentelemetry-integration)
|
||||
- [Configuration](#configuration)
|
||||
- [Google Cloud Telemetry](#google-cloud-telemetry)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Direct Export (Recommended)](#direct-export-recommended)
|
||||
- [Collector-Based Export (Advanced)](#collector-based-export-advanced)
|
||||
- [Local Telemetry](#local-telemetry)
|
||||
- [File-based Output (Recommended)](#file-based-output-recommended)
|
||||
- [Collector-Based Export (Advanced)](#collector-based-export-advanced-1)
|
||||
- [Logs and Metrics](#logs-and-metrics)
|
||||
- [Logs](#logs)
|
||||
- [Metrics](#metrics)
|
||||
|
||||
## Key Benefits
|
||||
|
||||
- **🔍 Usage Analytics**: Understand interaction patterns and feature adoption
|
||||
across your team
|
||||
- **⚡ Performance Monitoring**: Track response times, token consumption, and
|
||||
resource utilization
|
||||
- **🐛 Real-time Debugging**: Identify bottlenecks, failures, and error patterns
|
||||
as they occur
|
||||
- **📊 Workflow Optimization**: Make informed decisions to improve
|
||||
configurations and processes
|
||||
- **🏢 Enterprise Governance**: Monitor usage across teams, track costs, ensure
|
||||
compliance, and integrate with existing monitoring infrastructure
|
||||
|
||||
## OpenTelemetry Integration
|
||||
|
||||
Built on **[OpenTelemetry]** — the vendor-neutral, industry-standard
|
||||
observability framework — Gemini CLI's observability system provides:
|
||||
|
||||
- **Universal Compatibility**: Export to any OpenTelemetry backend (Google
|
||||
Cloud, Jaeger, Prometheus, Datadog, etc.)
|
||||
- **Standardized Data**: Use consistent formats and collection methods across
|
||||
your toolchain
|
||||
- **Future-Proof Integration**: Connect with existing and future observability
|
||||
infrastructure
|
||||
- **No Vendor Lock-in**: Switch between backends without changing your
|
||||
instrumentation
|
||||
|
||||
[OpenTelemetry]: https://opentelemetry.io/
|
||||
|
||||
## Enabling telemetry
|
||||
## Configuration
|
||||
|
||||
You can enable telemetry in multiple ways. Configuration is primarily managed via the [`.gemini/settings.json` file](./cli/configuration.md) and environment variables, but CLI flags can override these settings for a specific session.
|
||||
All telemetry behavior is controlled through your `.gemini/settings.json` file.
|
||||
These settings can be overridden by environment variables or CLI flags.
|
||||
|
||||
### Order of precedence
|
||||
| Setting | Environment Variable | CLI Flag | Description | Values | Default |
|
||||
| -------------- | -------------------------------- | -------------------------------------------------------- | ------------------------------------------------- | ----------------- | ----------------------- |
|
||||
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | `--telemetry` / `--no-telemetry` | Enable or disable telemetry | `true`/`false` | `false` |
|
||||
| `target` | `GEMINI_TELEMETRY_TARGET` | `--telemetry-target <local\|gcp>` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
|
||||
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | `--telemetry-otlp-endpoint <URL>` | OTLP collector endpoint | URL string | `http://localhost:4317` |
|
||||
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | `--telemetry-otlp-protocol <grpc\|http>` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
|
||||
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | `--telemetry-outfile <path>` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
|
||||
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | `--telemetry-log-prompts` / `--no-telemetry-log-prompts` | Include prompts in telemetry logs | `true`/`false` | `true` |
|
||||
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | - | Use external OTLP collector (advanced) | `true`/`false` | `false` |
|
||||
|
||||
The following lists the precedence for applying telemetry settings, with items listed higher having greater precedence:
|
||||
**Note on boolean environment variables:** For the boolean settings (`enabled`,
|
||||
`logPrompts`, `useCollector`), setting the corresponding environment variable to
|
||||
`true` or `1` will enable the feature. Any other value will disable it.
|
||||
|
||||
1. **CLI flags (for `gemini` command):**
|
||||
- `--telemetry` / `--no-telemetry`: Overrides `telemetry.enabled`.
|
||||
- `--telemetry-target <local|gcp>`: Overrides `telemetry.target`.
|
||||
- `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`.
|
||||
- `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`.
|
||||
- `--telemetry-outfile <path>`: Redirects telemetry output to a file. See [Exporting to a file](#exporting-to-a-file).
|
||||
For detailed information about all configuration options, see the
|
||||
[Configuration Guide](./cli/configuration.md).
|
||||
|
||||
1. **Environment variables:**
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint`.
|
||||
## Google Cloud Telemetry
|
||||
|
||||
1. **Workspace settings file (`.gemini/settings.json`):** Values from the `telemetry` object in this project-specific file.
|
||||
### Prerequisites
|
||||
|
||||
1. **User settings file (`~/.gemini/settings.json`):** Values from the `telemetry` object in this global user file.
|
||||
Before using either method below, complete these steps:
|
||||
|
||||
1. **Defaults:** applied if not set by any of the above.
|
||||
- `telemetry.enabled`: `false`
|
||||
- `telemetry.target`: `local`
|
||||
- `telemetry.otlpEndpoint`: `http://localhost:4317`
|
||||
- `telemetry.logPrompts`: `true`
|
||||
1. Set your Google Cloud project ID:
|
||||
- For telemetry in a separate project from inference:
|
||||
```bash
|
||||
export OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id"
|
||||
```
|
||||
- For telemetry in the same project as inference:
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
```
|
||||
|
||||
**For the `npm run telemetry -- --target=<gcp|local>` script:**
|
||||
The `--target` argument to this script _only_ overrides the `telemetry.target` for the duration and purpose of that script (i.e., choosing which collector to start). It does not permanently change your `settings.json`. The script will first look at `settings.json` for a `telemetry.target` to use as its default.
|
||||
2. Authenticate with Google Cloud:
|
||||
- If using a user account:
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
- If using a service account:
|
||||
```bash
|
||||
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account.json"
|
||||
```
|
||||
3. Make sure your account or service account has these IAM roles:
|
||||
- Cloud Trace Agent
|
||||
- Monitoring Metric Writer
|
||||
- Logs Writer
|
||||
|
||||
### Example settings
|
||||
4. Enable the required Google Cloud APIs (if not already enabled):
|
||||
```bash
|
||||
gcloud services enable \
|
||||
cloudtrace.googleapis.com \
|
||||
monitoring.googleapis.com \
|
||||
logging.googleapis.com \
|
||||
--project="$OTLP_GOOGLE_CLOUD_PROJECT"
|
||||
```
|
||||
|
||||
The following code can be added to your workspace (`.gemini/settings.json`) or user (`~/.gemini/settings.json`) settings to enable telemetry and send the output to Google Cloud:
|
||||
### Direct Export (Recommended)
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
},
|
||||
"sandbox": false
|
||||
}
|
||||
```
|
||||
Sends telemetry directly to Google Cloud services. No collector needed.
|
||||
|
||||
### Exporting to a file
|
||||
1. Enable telemetry in your `.gemini/settings.json`:
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Run Gemini CLI and send prompts.
|
||||
3. View logs and metrics:
|
||||
- Open the Google Cloud Console in your browser after sending prompts:
|
||||
- Logs: https://console.cloud.google.com/logs/
|
||||
- Metrics: https://console.cloud.google.com/monitoring/metrics-explorer
|
||||
- Traces: https://console.cloud.google.com/traces/list
|
||||
|
||||
You can export all telemetry data to a file for local inspection.
|
||||
### Collector-Based Export (Advanced)
|
||||
|
||||
To enable file export, use the `--telemetry-outfile` flag with a path to your desired output file. This must be run using `--telemetry-target=local`.
|
||||
For custom processing, filtering, or routing, use an OpenTelemetry collector to
|
||||
forward data to Google Cloud.
|
||||
|
||||
```bash
|
||||
# Set your desired output file path
|
||||
TELEMETRY_FILE=".gemini/telemetry.log"
|
||||
1. Configure your `.gemini/settings.json`:
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp",
|
||||
"useCollector": true
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Run the automation script:
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
This will:
|
||||
- Start a local OTEL collector that forwards to Google Cloud
|
||||
- Configure your workspace
|
||||
- Provide links to view traces, metrics, and logs in Google Cloud Console
|
||||
- Save collector logs to `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log`
|
||||
- Stop collector on exit (e.g. `Ctrl+C`)
|
||||
3. Run Gemini CLI and send prompts.
|
||||
4. View logs and metrics:
|
||||
- Open the Google Cloud Console in your browser after sending prompts:
|
||||
- Logs: https://console.cloud.google.com/logs/
|
||||
- Metrics: https://console.cloud.google.com/monitoring/metrics-explorer
|
||||
- Traces: https://console.cloud.google.com/traces/list
|
||||
- Open `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log` to view local
|
||||
collector logs.
|
||||
|
||||
# Run Gemini CLI with local telemetry
|
||||
# NOTE: --telemetry-otlp-endpoint="" is required to override the default
|
||||
# OTLP exporter and ensure telemetry is written to the local file.
|
||||
gemini --telemetry \
|
||||
--telemetry-target=local \
|
||||
--telemetry-otlp-endpoint="" \
|
||||
--telemetry-outfile="$TELEMETRY_FILE" \
|
||||
--prompt "What is OpenTelemetry?"
|
||||
```
|
||||
## Local Telemetry
|
||||
|
||||
## Running an OTEL Collector
|
||||
For local development and debugging, you can capture telemetry data locally:
|
||||
|
||||
An OTEL Collector is a service that receives, processes, and exports telemetry data.
|
||||
The CLI can send data using either the OTLP/gRPC or OTLP/HTTP protocol.
|
||||
You can specify which protocol to use via the `--telemetry-otlp-protocol` flag
|
||||
or the `telemetry.otlpProtocol` setting in your `settings.json` file. See the
|
||||
[configuration docs](./cli/configuration.md#--telemetry-otlp-protocol) for more
|
||||
details.
|
||||
### File-based Output (Recommended)
|
||||
|
||||
Learn more about OTEL exporter standard configuration in [documentation][otel-config-docs].
|
||||
1. Enable telemetry in your `.gemini/settings.json`:
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "",
|
||||
"outfile": ".gemini/telemetry.log"
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Run Gemini CLI and send prompts.
|
||||
3. View logs and metrics in the specified file (e.g., `.gemini/telemetry.log`).
|
||||
|
||||
[otel-config-docs]: https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
|
||||
### Collector-Based Export (Advanced)
|
||||
|
||||
### Local
|
||||
1. Run the automation script:
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
This will:
|
||||
- Download and start Jaeger and OTEL collector
|
||||
- Configure your workspace for local telemetry
|
||||
- Provide a Jaeger UI at http://localhost:16686
|
||||
- Save logs/metrics to `~/.gemini/tmp/<projectHash>/otel/collector.log`
|
||||
- Stop collector on exit (e.g. `Ctrl+C`)
|
||||
2. Run Gemini CLI and send prompts.
|
||||
3. View traces at http://localhost:16686 and logs/metrics in the collector log
|
||||
file.
|
||||
|
||||
Use the `npm run telemetry -- --target=local` command to automate the process of setting up a local telemetry pipeline, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib` (the OpenTelemetry Collector) and `jaeger` (The Jaeger UI for viewing traces). To use it:
|
||||
## Logs and Metrics
|
||||
|
||||
1. **Run the command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Download Jaeger and OTEL if needed.
|
||||
- Start a local Jaeger instance.
|
||||
- Start an OTEL collector configured to receive data from Gemini CLI.
|
||||
- Automatically enable telemetry in your workspace settings.
|
||||
- On exit, disable telemetry.
|
||||
|
||||
1. **View traces**:
|
||||
Open your web browser and navigate to **http://localhost:16686** to access the Jaeger UI. Here you can inspect detailed traces of Gemini CLI operations.
|
||||
|
||||
1. **Inspect logs and metrics**:
|
||||
The script redirects the OTEL collector output (which includes logs and metrics) to `~/.gemini/tmp/<projectHash>/otel/collector.log`. The script will provide links to view and a command to tail your telemetry data (traces, metrics, logs) locally.
|
||||
|
||||
1. **Stop the services**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector and Jaeger services.
|
||||
|
||||
### Google Cloud
|
||||
|
||||
Use the `npm run telemetry -- --target=gcp` command to automate setting up a local OpenTelemetry collector that forwards data to your Google Cloud project, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib`. To use it:
|
||||
|
||||
1. **Prerequisites**:
|
||||
- Have a Google Cloud project ID.
|
||||
- Export the `GOOGLE_CLOUD_PROJECT` environment variable to make it available to the OTEL collector.
|
||||
```bash
|
||||
export OTLP_GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
```
|
||||
- Authenticate with Google Cloud (e.g., run `gcloud auth application-default login` or ensure `GOOGLE_APPLICATION_CREDENTIALS` is set).
|
||||
- Ensure your Google Cloud account/service account has the necessary IAM roles: "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer".
|
||||
|
||||
1. **Run the command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Download the `otelcol-contrib` binary if needed.
|
||||
- Start an OTEL collector configured to receive data from Gemini CLI and export it to your specified Google Cloud project.
|
||||
- Automatically enable telemetry and disable sandbox mode in your workspace settings (`.gemini/settings.json`).
|
||||
- Provide direct links to view traces, metrics, and logs in your Google Cloud Console.
|
||||
- On exit (Ctrl+C), it will attempt to restore your original telemetry and sandbox settings.
|
||||
|
||||
1. **Run Gemini CLI:**
|
||||
In a separate terminal, run your Gemini CLI commands. This generates telemetry data that the collector captures.
|
||||
|
||||
1. **View telemetry in Google Cloud**:
|
||||
Use the links provided by the script to navigate to the Google Cloud Console and view your traces, metrics, and logs.
|
||||
|
||||
1. **Inspect local collector logs**:
|
||||
The script redirects the local OTEL collector output to `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log`. The script provides links to view and command to tail your collector logs locally.
|
||||
|
||||
1. **Stop the service**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector.
|
||||
|
||||
## Logs and metric reference
|
||||
|
||||
The following section describes the structure of logs and metrics generated for Gemini CLI.
|
||||
The following section describes the structure of logs and metrics generated for
|
||||
Gemini CLI.
|
||||
|
||||
- A `sessionId` is included as a common attribute on all logs and metrics.
|
||||
|
||||
### Logs
|
||||
|
||||
Logs are timestamped records of specific events. The following events are logged for Gemini CLI:
|
||||
Logs are timestamped records of specific events. The following events are logged
|
||||
for Gemini CLI:
|
||||
|
||||
- `gemini_cli.config`: This event occurs once at startup with the CLI's configuration.
|
||||
- `gemini_cli.config`: This event occurs once at startup with the CLI's
|
||||
configuration.
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `embedding_model` (string)
|
||||
@@ -174,12 +224,14 @@ Logs are timestamped records of specific events. The following events are logged
|
||||
- `file_filtering_respect_git_ignore` (boolean)
|
||||
- `debug_mode` (boolean)
|
||||
- `mcp_servers` (string)
|
||||
- `output_format` (string: "text" or "json")
|
||||
|
||||
- `gemini_cli.user_prompt`: This event occurs when a user submits a prompt.
|
||||
- **Attributes**:
|
||||
- `prompt_length` (int)
|
||||
- `prompt_id` (string)
|
||||
- `prompt` (string, this attribute is excluded if `log_prompts_enabled` is configured to be `false`)
|
||||
- `prompt` (string, this attribute is excluded if `log_prompts_enabled` is
|
||||
configured to be `false`)
|
||||
- `auth_type` (string)
|
||||
|
||||
- `gemini_cli.tool_call`: This event occurs for each function call.
|
||||
@@ -188,11 +240,27 @@ Logs are timestamped records of specific events. The following events are logged
|
||||
- `function_args`
|
||||
- `duration_ms`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", "auto_accept", or "modify", if applicable)
|
||||
- `decision` (string: "accept", "reject", "auto_accept", or "modify", if
|
||||
applicable)
|
||||
- `error` (if applicable)
|
||||
- `error_type` (if applicable)
|
||||
- `content_length` (int, if applicable)
|
||||
- `metadata` (if applicable, dictionary of string -> any)
|
||||
|
||||
- `gemini_cli.file_operation`: This event occurs for each file operation.
|
||||
- **Attributes**:
|
||||
- `tool_name` (string)
|
||||
- `operation` (string: "create", "read", "update")
|
||||
- `lines` (int, if applicable)
|
||||
- `mimetype` (string, if applicable)
|
||||
- `extension` (string, if applicable)
|
||||
- `programming_language` (string, if applicable)
|
||||
- `diff_stat` (json string, if applicable): A JSON string with the following members:
|
||||
- `ai_added_lines` (int)
|
||||
- `ai_removed_lines` (int)
|
||||
- `user_added_lines` (int)
|
||||
- `user_removed_lines` (int)
|
||||
|
||||
- `gemini_cli.api_request`: This event occurs when making a request to Gemini API.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
@@ -221,6 +289,15 @@ Logs are timestamped records of specific events. The following events are logged
|
||||
- `response_text` (if applicable)
|
||||
- `auth_type`
|
||||
|
||||
- `gemini_cli.tool_output_truncated`: This event occurs when the output of a tool call is too large and gets truncated.
|
||||
- **Attributes**:
|
||||
- `tool_name` (string)
|
||||
- `original_content_length` (int)
|
||||
- `truncated_content_length` (int)
|
||||
- `threshold` (int)
|
||||
- `lines` (int)
|
||||
- `prompt_id` (string)
|
||||
|
||||
- `gemini_cli.malformed_json_response`: This event occurs when a `generateJson` response from Gemini API cannot be parsed as a json.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
@@ -234,6 +311,15 @@ Logs are timestamped records of specific events. The following events are logged
|
||||
- `command` (string)
|
||||
- `subcommand` (string, if applicable)
|
||||
|
||||
- `gemini_cli.extension_enable`: This event occurs when an extension is enabled
|
||||
- `gemini_cli.extension_install`: This event occurs when an extension is installed
|
||||
- **Attributes**:
|
||||
- `extension_name` (string)
|
||||
- `extension_version` (string)
|
||||
- `extension_source` (string)
|
||||
- `status` (string)
|
||||
- `gemini_cli.extension_uninstall`: This event occurs when an extension is uninstalled
|
||||
|
||||
### Metrics
|
||||
|
||||
Metrics are numerical measurements of behavior over time. The following metrics are collected for Gemini CLI:
|
||||
@@ -273,8 +359,8 @@ Metrics are numerical measurements of behavior over time. The following metrics
|
||||
- `lines` (Int, if applicable): Number of lines in the file.
|
||||
- `mimetype` (string, if applicable): Mimetype of the file.
|
||||
- `extension` (string, if applicable): File extension of the file.
|
||||
- `ai_added_lines` (Int, if applicable): Number of lines added/changed by AI.
|
||||
- `ai_removed_lines` (Int, if applicable): Number of lines removed/changed by AI.
|
||||
- `model_added_lines` (Int, if applicable): Number of lines added/changed by the model.
|
||||
- `model_removed_lines` (Int, if applicable): Number of lines removed/changed by the model.
|
||||
- `user_added_lines` (Int, if applicable): Number of lines added/changed by user in AI proposed changes.
|
||||
- `user_removed_lines` (Int, if applicable): Number of lines removed/changed by user in AI proposed changes.
|
||||
- `programming_language` (string, if applicable): The programming language of the file.
|
||||
|
||||
@@ -51,7 +51,30 @@ The Gemini CLI uses the `mcpServers` configuration in your `settings.json` file
|
||||
|
||||
### Configure the MCP server in settings.json
|
||||
|
||||
You can configure MCP servers at the global level in the `~/.gemini/settings.json` file or in your project's root directory, create or open the `.gemini/settings.json` file. Within the file, add the `mcpServers` configuration block.
|
||||
You can configure MCP servers in your `settings.json` file in two main ways: through the top-level `mcpServers` object for specific server definitions, and through the `mcp` object for global settings that control server discovery and execution.
|
||||
|
||||
#### Global MCP Settings (`mcp`)
|
||||
|
||||
The `mcp` object in your `settings.json` allows you to define global rules for all MCP servers.
|
||||
|
||||
- **`mcp.serverCommand`** (string): A global command to start an MCP server.
|
||||
- **`mcp.allowed`** (array of strings): A list of MCP server names to allow. If this is set, only servers from this list (matching the keys in the `mcpServers` object) will be connected to.
|
||||
- **`mcp.excluded`** (array of strings): A list of MCP server names to exclude. Servers in this list will not be connected to.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"allowed": ["my-trusted-server"],
|
||||
"excluded": ["experimental-server"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Server-Specific Configuration (`mcpServers`)
|
||||
|
||||
The `mcpServers` object is where you define each individual MCP server you want the CLI to connect to.
|
||||
|
||||
### Configuration Structure
|
||||
|
||||
@@ -92,7 +115,7 @@ Each server configuration supports the following properties:
|
||||
- **`cwd`** (string): Working directory for Stdio transport
|
||||
- **`timeout`** (number): Request timeout in milliseconds (default: 600,000ms = 10 minutes)
|
||||
- **`trust`** (boolean): When `true`, bypasses all tool call confirmations for this server (default: `false`)
|
||||
- **`includeTools`** (string[]): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (whitelist behavior). If not specified, all tools from the server are enabled by default.
|
||||
- **`includeTools`** (string[]): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default.
|
||||
- **`excludeTools`** (string[]): List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded.
|
||||
|
||||
### OAuth Support for Remote MCP Servers
|
||||
@@ -667,9 +690,13 @@ await server.connect(transport);
|
||||
This can be included in `settings.json` under `mcpServers` with:
|
||||
|
||||
```json
|
||||
"nodeServer": {
|
||||
"command": "node",
|
||||
"args": ["filename.ts"],
|
||||
{
|
||||
"mcpServers": {
|
||||
"nodeServer": {
|
||||
"command": "node",
|
||||
"args": ["filename.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+81
-14
@@ -4,7 +4,9 @@ This document describes the `run_shell_command` tool for the Gemini CLI.
|
||||
|
||||
## Description
|
||||
|
||||
Use `run_shell_command` to interact with the underlying system, run scripts, or perform command-line operations. `run_shell_command` executes a given shell command. On Windows, the command will be executed with `cmd.exe /c`. On other platforms, the command will be executed with `bash -c`.
|
||||
Use `run_shell_command` to interact with the underlying system, run scripts, or perform command-line operations. `run_shell_command` executes a given shell command, including interactive commands that require user input (e.g., `vim`, `git rebase -i`) if the `tools.shell.enableInteractiveShell` setting is set to `true`.
|
||||
|
||||
On Windows, commands are executed with `cmd.exe /c`. On other platforms, they are executed with `bash -c`.
|
||||
|
||||
### Arguments
|
||||
|
||||
@@ -53,10 +55,67 @@ Start a background server:
|
||||
run_shell_command(command="npm run dev &", description="Start development server in background")
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure the behavior of the `run_shell_command` tool by modifying your `settings.json` file or by using the `/settings` command in the Gemini CLI.
|
||||
|
||||
### Enabling Interactive Commands
|
||||
|
||||
To enable interactive commands, you need to set the `tools.shell.enableInteractiveShell` setting to `true`. This will use `node-pty` for shell command execution, which allows for interactive sessions. If `node-pty` is not available, it will fall back to the `child_process` implementation, which does not support interactive commands.
|
||||
|
||||
**Example `settings.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"shell": {
|
||||
"enableInteractiveShell": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Showing Color in Output
|
||||
|
||||
To show color in the shell output, you need to set the `tools.shell.showColor` setting to `true`. **Note: This setting only applies when `tools.shell.enableInteractiveShell` is enabled.**
|
||||
|
||||
**Example `settings.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"shell": {
|
||||
"showColor": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Setting the Pager
|
||||
|
||||
You can set a custom pager for the shell output by setting the `tools.shell.pager` setting. The default pager is `cat`. **Note: This setting only applies when `tools.shell.enableInteractiveShell` is enabled.**
|
||||
|
||||
**Example `settings.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"shell": {
|
||||
"pager": "less"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interactive Commands
|
||||
|
||||
The `run_shell_command` tool now supports interactive commands by integrating a pseudo-terminal (pty). This allows you to run commands that require real-time user input, such as text editors (`vim`, `nano`), terminal-based UIs (`htop`), and interactive version control operations (`git rebase -i`).
|
||||
|
||||
When an interactive command is running, you can send input to it from the Gemini CLI. To focus on the interactive shell, press `ctrl+f`. The terminal output, including complex TUIs, will be rendered correctly.
|
||||
|
||||
## Important notes
|
||||
|
||||
- **Security:** Be cautious when executing commands, especially those constructed from user input, to prevent security vulnerabilities.
|
||||
- **Interactive commands:** Avoid commands that require interactive user input, as this can cause the tool to hang. Use non-interactive flags if available (e.g., `npm init -y`).
|
||||
- **Error handling:** Check the `Stderr`, `Error`, and `Exit Code` fields to determine if a command executed successfully.
|
||||
- **Background processes:** When a command is run in the background with `&`, the tool will return immediately and the process will continue to run in the background. The `Background PIDs` field will contain the process ID of the background process.
|
||||
|
||||
@@ -66,16 +125,16 @@ When `run_shell_command` executes a command, it sets the `GEMINI_CLI=1` environm
|
||||
|
||||
## Command Restrictions
|
||||
|
||||
You can restrict the commands that can be executed by the `run_shell_command` tool by using the `coreTools` and `excludeTools` settings in your configuration file.
|
||||
You can restrict the commands that can be executed by the `run_shell_command` tool by using the `tools.core` and `tools.exclude` settings in your configuration file.
|
||||
|
||||
- `coreTools`: To restrict `run_shell_command` to a specific set of commands, add entries to the `coreTools` list in the format `run_shell_command(<command>)`. For example, `"coreTools": ["run_shell_command(git)"]` will only allow `git` commands. Including the generic `run_shell_command` acts as a wildcard, allowing any command not explicitly blocked.
|
||||
- `excludeTools`: To block specific commands, add entries to the `excludeTools` list in the format `run_shell_command(<command>)`. For example, `"excludeTools": ["run_shell_command(rm)"]` will block `rm` commands.
|
||||
- `tools.core`: To restrict `run_shell_command` to a specific set of commands, add entries to the `core` list under the `tools` category in the format `run_shell_command(<command>)`. For example, `"tools": {"core": ["run_shell_command(git)"]}` will only allow `git` commands. Including the generic `run_shell_command` acts as a wildcard, allowing any command not explicitly blocked.
|
||||
- `tools.exclude`: To block specific commands, add entries to the `exclude` list under the `tools` category in the format `run_shell_command(<command>)`. For example, `"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands.
|
||||
|
||||
The validation logic is designed to be secure and flexible:
|
||||
|
||||
1. **Command Chaining Disabled**: The tool automatically splits commands chained with `&&`, `||`, or `;` and validates each part separately. If any part of the chain is disallowed, the entire command is blocked.
|
||||
2. **Prefix Matching**: The tool uses prefix matching. For example, if you allow `git`, you can run `git status` or `git log`.
|
||||
3. **Blocklist Precedence**: The `excludeTools` list is always checked first. If a command matches a blocked prefix, it will be denied, even if it also matches an allowed prefix in `coreTools`.
|
||||
3. **Blocklist Precedence**: The `tools.exclude` list is always checked first. If a command matches a blocked prefix, it will be denied, even if it also matches an allowed prefix in `tools.core`.
|
||||
|
||||
### Command Restriction Examples
|
||||
|
||||
@@ -85,7 +144,9 @@ To allow only `git` and `npm` commands, and block all others:
|
||||
|
||||
```json
|
||||
{
|
||||
"coreTools": ["run_shell_command(git)", "run_shell_command(npm)"]
|
||||
"tools": {
|
||||
"core": ["run_shell_command(git)", "run_shell_command(npm)"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -99,8 +160,10 @@ To block `rm` and allow all other commands:
|
||||
|
||||
```json
|
||||
{
|
||||
"coreTools": ["run_shell_command"],
|
||||
"excludeTools": ["run_shell_command(rm)"]
|
||||
"tools": {
|
||||
"core": ["run_shell_command"],
|
||||
"exclude": ["run_shell_command(rm)"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -110,12 +173,14 @@ To block `rm` and allow all other commands:
|
||||
|
||||
**Blocklist takes precedence**
|
||||
|
||||
If a command prefix is in both `coreTools` and `excludeTools`, it will be blocked.
|
||||
If a command prefix is in both `tools.core` and `tools.exclude`, it will be blocked.
|
||||
|
||||
```json
|
||||
{
|
||||
"coreTools": ["run_shell_command(git)"],
|
||||
"excludeTools": ["run_shell_command(git push)"]
|
||||
"tools": {
|
||||
"core": ["run_shell_command(git)"],
|
||||
"exclude": ["run_shell_command(git push)"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -124,11 +189,13 @@ If a command prefix is in both `coreTools` and `excludeTools`, it will be blocke
|
||||
|
||||
**Block all shell commands**
|
||||
|
||||
To block all shell commands, add the `run_shell_command` wildcard to `excludeTools`:
|
||||
To block all shell commands, add the `run_shell_command` wildcard to `tools.exclude`:
|
||||
|
||||
```json
|
||||
{
|
||||
"excludeTools": ["run_shell_command"]
|
||||
"tools": {
|
||||
"exclude": ["run_shell_command"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+7
-7
@@ -8,7 +8,7 @@ This article outlines the specific terms and privacy policies applicable for dif
|
||||
|
||||
Your authentication method refers to the method you use to log into and access the Gemini CLI. There are four ways to authenticate:
|
||||
|
||||
- Logging in with your Google account to Gemini Code Assist for Individuals
|
||||
- Logging in with your Google account to Gemini Code Assist for individuals
|
||||
- Logging in with your Google account to Gemini Code Assist for Standard, or Enterprise Users
|
||||
- Using an API key with Gemini Developer
|
||||
- Using an API key with Vertex AI GenAI API
|
||||
@@ -17,18 +17,18 @@ For each of these four methods of authentication, different Terms of Service and
|
||||
|
||||
| Authentication | Account | Terms of Service | Privacy Notice |
|
||||
| :---------------------------- | :------------------ | :------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Gemini Code Assist via Google | Individual | [Google Terms of Service](https://policies.google.com/terms?hl=en-US) | [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals) |
|
||||
| Gemini Code Assist via Google | Individual | [Google Terms of Service](https://policies.google.com/terms?hl=en-US) | [Gemini Code Assist Privacy Notice for individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals) |
|
||||
| Gemini Code Assist via Google | Standard/Enterprise | [Google Cloud Platform Terms of Service](https://cloud.google.com/terms) | [Gemini Code Assist Privacy Notice for Standard and Enterprise](https://cloud.google.com/gemini/docs/codeassist/security-privacy-compliance#standard_and_enterprise_data_protection_and_privacy) |
|
||||
| Gemini Developer API | Unpaid | [Gemini API Terms of Service - Unpaid Services](https://ai.google.dev/gemini-api/terms#unpaid-services) | [Google Privacy Policy](https://policies.google.com/privacy) |
|
||||
| Gemini Developer API | Paid | [Gemini API Terms of Service - Paid Services](https://ai.google.dev/gemini-api/terms#paid-services) | [Google Privacy Policy](https://policies.google.com/privacy) |
|
||||
| Vertex AI Gen API | | [Google Cloud Platform Service Terms](https://cloud.google.com/terms/service-terms/) | [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) |
|
||||
|
||||
## 1. If you have logged in with your Google account to Gemini Code Assist for Individuals
|
||||
## 1. If you have logged in with your Google account to Gemini Code Assist for individuals
|
||||
|
||||
For users who use their Google account to access [Gemini Code Assist for Individuals](https://developers.google.com/gemini-code-assist/docs/overview#supported-features-gca), these Terms of Service and Privacy Notice documents apply:
|
||||
For users who use their Google account to access [Gemini Code Assist for individuals](https://developers.google.com/gemini-code-assist/docs/overview#supported-features-gca), these Terms of Service and Privacy Notice documents apply:
|
||||
|
||||
- **Terms of Service:** Your use of the Gemini CLI is governed by the [Google Terms of Service](https://policies.google.com/terms?hl=en-US).
|
||||
- **Privacy Notice:** The collection and use of your data is described in the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals).
|
||||
- **Privacy Notice:** The collection and use of your data is described in the [Gemini Code Assist Privacy Notice for individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals).
|
||||
|
||||
## 2. If you have logged in with your Google account to Gemini Code Assist for Standard, or Enterprise Users
|
||||
|
||||
@@ -65,7 +65,7 @@ Whether your code, including prompts and answers, is used to train Google's mode
|
||||
|
||||
By default (if you have not opted out):
|
||||
|
||||
- **Google account with Gemini Code Assist for Individuals**: Yes. When you use your personal Google account, the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals) applies. Under this notice,
|
||||
- **Google account with Gemini Code Assist for individuals**: Yes. When you use your personal Google account, the [Gemini Code Assist Privacy Notice for individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals) applies. Under this notice,
|
||||
your **prompts, answers, and related code are collected** and may be used to improve Google's products, including for model training.
|
||||
- **Google account with Gemini Code Assist for Standard, or Enterprise**: No. For these accounts, your data is governed by the [Gemini Code Assist Privacy Notices](https://cloud.google.com/gemini/docs/codeassist/security-privacy-compliance#standard_and_enterprise_data_protection_and_privacy) terms, which treat your inputs as confidential. Your **prompts, answers, and related code are not collected** and are not used to train models.
|
||||
- **Gemini API key via the Gemini Developer API**: Whether your code is collected or used depends on whether you are using an unpaid or paid service.
|
||||
@@ -81,7 +81,7 @@ The **Usage Statistics** setting is the single control for all optional data col
|
||||
|
||||
The data it collects depends on your account and authentication type:
|
||||
|
||||
- **Google account with Gemini Code Assist for Individuals**: When enabled, this setting allows Google to collect both anonymous telemetry (for example, commands run and performance metrics) and **your prompts and answers, including code,** for model improvement.
|
||||
- **Google account with Gemini Code Assist for individuals**: When enabled, this setting allows Google to collect both anonymous telemetry (for example, commands run and performance metrics) and **your prompts and answers, including code,** for model improvement.
|
||||
- **Google account with Gemini Code Assist for Standard, or Enterprise**: This setting only controls the collection of anonymous telemetry. Your prompts and answers, including code, are never collected, regardless of this setting.
|
||||
- **Gemini API key via the Gemini Developer API**:
|
||||
**Unpaid services**: When enabled, this setting allows Google to collect both anonymous telemetry (like commands run and performance metrics) and **your prompts and answers, including code,** for model improvement. When disabled we will use your data as described in [How Google Uses Your Data](https://ai.google.dev/gemini-api/terms#data-use-unpaid).
|
||||
|
||||
@@ -72,7 +72,7 @@ This guide provides solutions to common issues and debugging tips, including top
|
||||
- **DEBUG mode not working from project .env file**
|
||||
- **Issue:** Setting `DEBUG=true` in a project's `.env` file doesn't enable debug mode for gemini-cli.
|
||||
- **Cause:** The `DEBUG` and `DEBUG_MODE` variables are automatically excluded from project `.env` files to prevent interference with gemini-cli behavior.
|
||||
- **Solution:** Use a `.gemini/.env` file instead, or configure the `excludedProjectEnvVars` setting in your `settings.json` to exclude fewer variables.
|
||||
- **Solution:** Use a `.gemini/.env` file instead, or configure the `advanced.excludedEnvVars` setting in your `settings.json` to exclude fewer variables.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Trusted Folders
|
||||
|
||||
The Trusted Folders feature is a security setting that gives you control over which projects can use the full capabilities of the Gemini CLI. It prevents potentially malicious code from running by asking you to approve a folder before the CLI loads any project-specific configurations from it.
|
||||
|
||||
## Enabling the Feature
|
||||
|
||||
The Trusted Folders feature is **disabled by default**. To use it, you must first enable it in your settings.
|
||||
|
||||
Add the following to your user `settings.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"security": {
|
||||
"folderTrust": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works: The Trust Dialog
|
||||
|
||||
Once the feature is enabled, the first time you run the Gemini CLI from a folder, a dialog will automatically appear, prompting you to make a choice:
|
||||
|
||||
- **Trust folder**: Grants full trust to the current folder (e.g., `my-project`).
|
||||
- **Trust parent folder**: Grants trust to the parent directory (e.g., `safe-projects`), which automatically trusts all of its subdirectories as well. This is useful if you keep all your safe projects in one place.
|
||||
- **Don't trust**: Marks the folder as untrusted. The CLI will operate in a restricted "safe mode."
|
||||
|
||||
Your choice is saved in a central file (`~/.gemini/trustedFolders.json`), so you will only be asked once per folder.
|
||||
|
||||
## Why Trust Matters: The Impact of an Untrusted Workspace
|
||||
|
||||
When a folder is **untrusted**, the Gemini CLI runs in a restricted "safe mode" to protect you. In this mode, the following features are disabled:
|
||||
|
||||
1. **Workspace Settings are Ignored**: The CLI will **not** load the `.gemini/settings.json` file from the project. This prevents the loading of custom tools and other potentially dangerous configurations.
|
||||
|
||||
2. **Environment Variables are Ignored**: The CLI will **not** load any `.env` files from the project.
|
||||
|
||||
3. **Extension Management is Restricted**: You **cannot install, update, or uninstall** extensions.
|
||||
|
||||
4. **Tool Auto-Acceptance is Disabled**: You will always be prompted before any tool is run, even if you have auto-acceptance enabled globally.
|
||||
|
||||
5. **Automatic Memory Loading is Disabled**: The CLI will not automatically load files into context from directories specified in local settings.
|
||||
|
||||
Granting trust to a folder unlocks the full functionality of the Gemini CLI for that workspace.
|
||||
|
||||
## Managing Your Trust Settings
|
||||
|
||||
If you need to change a decision or see all your settings, you have a couple of options:
|
||||
|
||||
- **Change the Current Folder's Trust**: Run the `/permissions` command from within the CLI. This will bring up the same interactive dialog, allowing you to change the trust level for the current folder.
|
||||
|
||||
- **View All Trust Rules**: To see a complete list of all your trusted and untrusted folder rules, you can inspect the contents of the `~/.gemini/trustedFolders.json` file in your home directory.
|
||||
|
||||
## The Trust Check Process (Advanced)
|
||||
|
||||
For advanced users, it's helpful to know the exact order of operations for how trust is determined:
|
||||
|
||||
1. **IDE Trust Signal**: If you are using the [IDE Integration](./ide-integration.md), the CLI first asks the IDE if the workspace is trusted. The IDE's response takes highest priority.
|
||||
|
||||
2. **Local Trust File**: If the IDE is not connected, the CLI checks the central `~/.gemini/trustedFolders.json` file.
|
||||
+27
-10
@@ -4,16 +4,34 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import esbuild from 'esbuild';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
let esbuild;
|
||||
try {
|
||||
esbuild = (await import('esbuild')).default;
|
||||
} catch (_error) {
|
||||
console.warn('esbuild not available, skipping bundle step');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkg = require(path.resolve(__dirname, 'package.json'));
|
||||
|
||||
const external = [
|
||||
'@lydell/node-pty',
|
||||
'node-pty',
|
||||
'@lydell/node-pty-darwin-arm64',
|
||||
'@lydell/node-pty-darwin-x64',
|
||||
'@lydell/node-pty-linux-x64',
|
||||
'@lydell/node-pty-win32-arm64',
|
||||
'@lydell/node-pty-win32-x64',
|
||||
];
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
entryPoints: ['packages/cli/index.ts'],
|
||||
@@ -21,15 +39,7 @@ esbuild
|
||||
outfile: 'bundle/gemini.js',
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
external: [
|
||||
'@lydell/node-pty',
|
||||
'node-pty',
|
||||
'@lydell/node-pty-darwin-arm64',
|
||||
'@lydell/node-pty-darwin-x64',
|
||||
'@lydell/node-pty-linux-x64',
|
||||
'@lydell/node-pty-win32-arm64',
|
||||
'@lydell/node-pty-win32-x64',
|
||||
],
|
||||
external,
|
||||
alias: {
|
||||
'is-in-ci': path.resolve(
|
||||
__dirname,
|
||||
@@ -43,5 +53,12 @@ esbuild
|
||||
js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url); globalThis.__filename = require('url').fileURLToPath(import.meta.url); globalThis.__dirname = require('path').dirname(globalThis.__filename);`,
|
||||
},
|
||||
loader: { '.node': 'file' },
|
||||
metafile: true,
|
||||
write: true,
|
||||
})
|
||||
.then(({ metafile }) => {
|
||||
if (process.env.DEV === 'true') {
|
||||
writeFileSync('./bundle/esbuild.json', JSON.stringify(metafile, null, 2));
|
||||
}
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
|
||||
@@ -35,6 +35,7 @@ export default tseslint.config(
|
||||
'bundle/**',
|
||||
'package/bundle/**',
|
||||
'.integration-tests/**',
|
||||
'dist/**',
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Ink Library Screen Reader Guidance
|
||||
|
||||
When building custom components, it's important to keep accessibility in mind. While Ink provides the building blocks, ensuring your components are accessible will make your CLIs usable by a wider audience.
|
||||
|
||||
## General Principles
|
||||
|
||||
Provide screen reader-friendly output: Use the useIsScreenReaderEnabled hook to detect if a screen reader is active. You can then render a more descriptive output for screen reader users.
|
||||
Leverage ARIA props: For components that have a specific role (e.g., a checkbox or a button), use the aria-role, aria-state, and aria-label props on <Box> and <Text> to provide semantic meaning to screen readers.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "context-example",
|
||||
"version": "1.0.0",
|
||||
"contextFileName": "GEMINI.md"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
describe('Ctrl+C exit', () => {
|
||||
// (#9782) Temporarily disabling on windows because it is failing on main and every
|
||||
// PR, which is potentially hiding other failures
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'should exit gracefully on second Ctrl+C',
|
||||
async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should exit gracefully on second Ctrl+C');
|
||||
|
||||
const { ptyProcess, promise } = rig.runInteractive();
|
||||
|
||||
let output = '';
|
||||
ptyProcess.onData((data) => {
|
||||
output += data;
|
||||
});
|
||||
|
||||
// Wait for the app to be ready by looking for the initial prompt indicator
|
||||
await rig.poll(() => output.includes('▶'), 5000, 100);
|
||||
|
||||
// Send first Ctrl+C
|
||||
ptyProcess.write(String.fromCharCode(3));
|
||||
|
||||
// Wait for the exit prompt
|
||||
await rig.poll(
|
||||
() => output.includes('Press Ctrl+C again to exit'),
|
||||
1500,
|
||||
50,
|
||||
);
|
||||
|
||||
// Send second Ctrl+C
|
||||
ptyProcess.write(String.fromCharCode(3));
|
||||
|
||||
const result = await promise;
|
||||
|
||||
// Expect a graceful exit (code 0)
|
||||
expect(
|
||||
result.exitCode,
|
||||
`Process exited with code ${result.exitCode}. Output: ${result.output}`,
|
||||
).toBe(0);
|
||||
|
||||
// Check that the quitting message is displayed
|
||||
const quittingMessage = 'Agent powering down. Goodbye!';
|
||||
// The regex below is intentionally matching the ESC control character (\x1b)
|
||||
// to strip ANSI color codes from the terminal output.
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
expect(cleanOutput).toContain(quittingMessage);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'should exit gracefully on second Ctrl+C when calling a tool',
|
||||
async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup(
|
||||
'should exit gracefully on second Ctrl+C when calling a tool',
|
||||
);
|
||||
|
||||
const childProcessFile = 'child_process_file.txt';
|
||||
rig.createFile(
|
||||
'wait.js',
|
||||
`setTimeout(() => require('fs').writeFileSync('${childProcessFile}', 'done'), 5000)`,
|
||||
);
|
||||
|
||||
const { ptyProcess, promise } = rig.runInteractive();
|
||||
|
||||
let output = '';
|
||||
ptyProcess.onData((data) => {
|
||||
output += data;
|
||||
});
|
||||
|
||||
// Wait for the app to be ready by looking for the initial prompt indicator
|
||||
await rig.poll(() => output.includes('▶'), 5000, 100);
|
||||
|
||||
ptyProcess.write('use the tool to run "node -e wait.js"\n');
|
||||
|
||||
await rig.poll(() => output.includes('Shell'), 5000, 100);
|
||||
|
||||
// Send first Ctrl+C
|
||||
ptyProcess.write(String.fromCharCode(3));
|
||||
|
||||
// Wait for the exit prompt
|
||||
await rig.poll(
|
||||
() => output.includes('Press Ctrl+C again to exit'),
|
||||
1500,
|
||||
50,
|
||||
);
|
||||
|
||||
// Send second Ctrl+C
|
||||
ptyProcess.write(String.fromCharCode(3));
|
||||
|
||||
const result = await promise;
|
||||
|
||||
// Expect a graceful exit (code 0)
|
||||
expect(
|
||||
result.exitCode,
|
||||
`Process exited with code ${result.exitCode}. Output: ${result.output}`,
|
||||
).toBe(0);
|
||||
|
||||
// Check that the quitting message is displayed
|
||||
const quittingMessage = 'Agent powering down. Goodbye!';
|
||||
// The regex below is intentionally matching the ESC control character (\x1b)
|
||||
// to strip ANSI color codes from the terminal output.
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
expect(cleanOutput).toContain(quittingMessage);
|
||||
|
||||
// Check that the child process was terminated and did not create the file.
|
||||
const childProcessFileExists = fs.existsSync(
|
||||
path.join(rig.testDir!, childProcessFile),
|
||||
);
|
||||
expect(
|
||||
childProcessFileExists,
|
||||
'Child process file should not exist',
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const extension = `{
|
||||
"name": "test-extension",
|
||||
"version": "0.0.1"
|
||||
}`;
|
||||
|
||||
const extensionUpdate = `{
|
||||
"name": "test-extension",
|
||||
"version": "0.0.2"
|
||||
}`;
|
||||
|
||||
test('installs a local extension, verifies a command, and updates it', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('extension install test');
|
||||
const testServerPath = join(rig.testDir!, 'gemini-extension.json');
|
||||
writeFileSync(testServerPath, extension);
|
||||
try {
|
||||
await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
const result = await rig.runCommand(
|
||||
['extensions', 'install', `--path=${rig.testDir!}`],
|
||||
{ stdin: 'y\n' },
|
||||
);
|
||||
expect(result).toContain('test-extension');
|
||||
|
||||
const listResult = await rig.runCommand(['extensions', 'list']);
|
||||
expect(listResult).toContain('test-extension');
|
||||
writeFileSync(testServerPath, extensionUpdate);
|
||||
const updateResult = await rig.runCommand([
|
||||
'extensions',
|
||||
'update',
|
||||
`test-extension`,
|
||||
]);
|
||||
expect(updateResult).toContain('0.0.2');
|
||||
|
||||
await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
|
||||
|
||||
await rig.cleanup();
|
||||
});
|
||||
@@ -1,201 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import * as net from 'node:net';
|
||||
import * as child_process from 'node:child_process';
|
||||
import { IdeClient } from '../packages/core/src/ide/ide-client.js';
|
||||
|
||||
import { TestMcpServer } from './test-mcp-server.js';
|
||||
|
||||
describe.skip('IdeClient', () => {
|
||||
it('reads port from file and connects', async () => {
|
||||
const server = new TestMcpServer();
|
||||
const port = await server.start();
|
||||
const pid = process.pid;
|
||||
const portFile = path.join(os.tmpdir(), `gemini-ide-server-${pid}.json`);
|
||||
fs.writeFileSync(portFile, JSON.stringify({ port }));
|
||||
process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'] = process.cwd();
|
||||
process.env['TERM_PROGRAM'] = 'vscode';
|
||||
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
await ideClient.connect();
|
||||
|
||||
expect(ideClient.getConnectionStatus()).toEqual({
|
||||
status: 'connected',
|
||||
details: undefined,
|
||||
});
|
||||
|
||||
fs.unlinkSync(portFile);
|
||||
await server.stop();
|
||||
delete process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'];
|
||||
});
|
||||
});
|
||||
|
||||
const getFreePort = (): Promise<number> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.unref();
|
||||
server.on('error', reject);
|
||||
server.listen(0, () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
server.close(() => {
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('IdeClient fallback connection logic', () => {
|
||||
let server: TestMcpServer;
|
||||
let envPort: number;
|
||||
let pid: number;
|
||||
let portFile: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
pid = process.pid;
|
||||
portFile = path.join(os.tmpdir(), `gemini-ide-server-${pid}.json`);
|
||||
server = new TestMcpServer();
|
||||
envPort = await server.start();
|
||||
process.env['GEMINI_CLI_IDE_SERVER_PORT'] = String(envPort);
|
||||
process.env['TERM_PROGRAM'] = 'vscode';
|
||||
process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'] = process.cwd();
|
||||
// Reset instance
|
||||
(IdeClient as unknown as { instance: IdeClient | undefined }).instance =
|
||||
undefined;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
delete process.env['GEMINI_CLI_IDE_SERVER_PORT'];
|
||||
delete process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'];
|
||||
if (fs.existsSync(portFile)) {
|
||||
fs.unlinkSync(portFile);
|
||||
}
|
||||
});
|
||||
|
||||
it('connects using env var when port file does not exist', async () => {
|
||||
// Ensure port file doesn't exist
|
||||
if (fs.existsSync(portFile)) {
|
||||
fs.unlinkSync(portFile);
|
||||
}
|
||||
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
await ideClient.connect();
|
||||
|
||||
expect(ideClient.getConnectionStatus()).toEqual({
|
||||
status: 'connected',
|
||||
details: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to env var when connection with port from file fails', async () => {
|
||||
const filePort = await getFreePort();
|
||||
// Write port file with a port that is not listening
|
||||
fs.writeFileSync(portFile, JSON.stringify({ port: filePort }));
|
||||
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
await ideClient.connect();
|
||||
|
||||
expect(ideClient.getConnectionStatus()).toEqual({
|
||||
status: 'connected',
|
||||
details: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('getIdeProcessId', () => {
|
||||
let child: child_process.ChildProcess;
|
||||
|
||||
afterEach(() => {
|
||||
if (child) {
|
||||
child.kill();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return the pid of the parent process', async () => {
|
||||
// We need to spawn a child process that will run the test
|
||||
// so that we can check that getIdeProcessId returns the pid of the parent
|
||||
const parentPid = process.pid;
|
||||
const output = await new Promise<string>((resolve, reject) => {
|
||||
child = child_process.spawn(
|
||||
'node',
|
||||
[
|
||||
'-e',
|
||||
`
|
||||
const { getIdeProcessId } = require('../packages/core/src/ide/process-utils.js');
|
||||
getIdeProcessId().then(pid => console.log(pid));
|
||||
`,
|
||||
],
|
||||
{
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
|
||||
let out = '';
|
||||
child.stdout?.on('data', (data) => {
|
||||
out += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(out.trim());
|
||||
} else {
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
expect(parseInt(output, 10)).toBe(parentPid);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('IdeClient with proxy', () => {
|
||||
let mcpServer: TestMcpServer;
|
||||
let proxyServer: net.Server;
|
||||
let mcpServerPort: number;
|
||||
let proxyServerPort: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
mcpServer = new TestMcpServer();
|
||||
mcpServerPort = await mcpServer.start();
|
||||
|
||||
proxyServer = net.createServer().listen();
|
||||
proxyServerPort = (proxyServer.address() as net.AddressInfo).port;
|
||||
|
||||
vi.stubEnv('GEMINI_CLI_IDE_SERVER_PORT', String(mcpServerPort));
|
||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', process.cwd());
|
||||
|
||||
// Reset instance
|
||||
(IdeClient as unknown as { instance: IdeClient | undefined }).instance =
|
||||
undefined;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
(await IdeClient.getInstance()).disconnect();
|
||||
await mcpServer.stop();
|
||||
proxyServer.close();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should connect to IDE server when HTTP_PROXY, HTTPS_PROXY and NO_PROXY are set', async () => {
|
||||
vi.stubEnv('HTTP_PROXY', `http://localhost:${proxyServerPort}`);
|
||||
vi.stubEnv('HTTPS_PROXY', `http://localhost:${proxyServerPort}`);
|
||||
vi.stubEnv('NO_PROXY', 'example.com,127.0.0.1,::1');
|
||||
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
await ideClient.connect();
|
||||
|
||||
expect(ideClient.getConnectionStatus()).toEqual({
|
||||
status: 'connected',
|
||||
details: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { expect, describe, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('JSON output', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('json-output-test');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('should return a valid JSON with response and stats', async () => {
|
||||
const result = await rig.run(
|
||||
'What is the capital of France?',
|
||||
'--output-format',
|
||||
'json',
|
||||
);
|
||||
const parsed = JSON.parse(result);
|
||||
|
||||
expect(parsed).toHaveProperty('response');
|
||||
expect(typeof parsed.response).toBe('string');
|
||||
expect(parsed.response.toLowerCase()).toContain('paris');
|
||||
|
||||
expect(parsed).toHaveProperty('stats');
|
||||
expect(typeof parsed.stats).toBe('object');
|
||||
});
|
||||
|
||||
it('should return a JSON error for enforced auth mismatch before running', async () => {
|
||||
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
|
||||
await rig.setup('json-output-auth-mismatch', {
|
||||
settings: {
|
||||
security: { auth: { enforcedType: 'gemini-api-key' } },
|
||||
},
|
||||
});
|
||||
|
||||
let thrown: Error | undefined;
|
||||
try {
|
||||
await rig.run('Hello', '--output-format', 'json');
|
||||
expect.fail('Expected process to exit with error');
|
||||
} catch (e) {
|
||||
thrown = e as Error;
|
||||
} finally {
|
||||
delete process.env['GOOGLE_GENAI_USE_GCA'];
|
||||
}
|
||||
|
||||
expect(thrown).toBeDefined();
|
||||
const message = (thrown as Error).message;
|
||||
|
||||
// Use a regex to find the first complete JSON object in the string
|
||||
const jsonMatch = message.match(/{[\s\S]*}/);
|
||||
|
||||
// Fail if no JSON-like text was found
|
||||
expect(
|
||||
jsonMatch,
|
||||
'Expected to find a JSON object in the error output',
|
||||
).toBeTruthy();
|
||||
|
||||
let payload;
|
||||
try {
|
||||
// Parse the matched JSON string
|
||||
payload = JSON.parse(jsonMatch![0]);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse the following JSON:', jsonMatch![0]);
|
||||
throw new Error(
|
||||
`Test failed: Could not parse JSON from error message. Details: ${parseError}`,
|
||||
);
|
||||
}
|
||||
|
||||
expect(payload.error).toBeDefined();
|
||||
expect(payload.error.type).toBe('Error');
|
||||
expect(payload.error.code).toBe(1);
|
||||
expect(payload.error.message).toContain(
|
||||
'configured auth type is gemini-api-key',
|
||||
);
|
||||
expect(payload.error.message).toContain(
|
||||
'current auth type is oauth-personal',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@ describe('list_directory', () => {
|
||||
50, // check every 50ms
|
||||
);
|
||||
|
||||
const prompt = `Can you list the files in the current directory. Display them in the style of 'ls'`;
|
||||
const prompt = `Can you list the files in the current directory.`;
|
||||
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
|
||||
@@ -5,14 +5,26 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* This test verifies we can match maximum schema depth errors from Gemini
|
||||
* and then detect and warn about the potential tools that caused the error.
|
||||
* This test verifies we can provide MCP tools with recursive input schemas
|
||||
* (in JSON, using the $ref keyword) and both the GenAI SDK and the Gemini
|
||||
* API calls succeed. Note that prior to
|
||||
* https://github.com/googleapis/js-genai/commit/36f6350705ecafc47eaea3f3eecbcc69512edab7#diff-fdde9372aec859322b7c5a5efe467e0ad25a57210c7229724586ee90ea4f5a30
|
||||
* the Gemini API call would fail for such tools because the schema was
|
||||
* passed not as a JSON string but using the Gemini API's tool parameter
|
||||
* schema object which has stricter typing and recursion restrictions.
|
||||
* If this test fails, it's likely because either the GenAI SDK or Gemini API
|
||||
* has become more restrictive about the type of tool parameter schemas that
|
||||
* are accepted. If this occurs: Gemini CLI previously attempted to detect
|
||||
* such tools and proactively remove them from the set of tools provided in
|
||||
* the Gemini API call (as FunctionDeclaration objects). It may be appropriate
|
||||
* to resurrect that behavior but note that it's difficult to keep the
|
||||
* GCLI filters in sync with the Gemini API restrictions and behavior.
|
||||
*/
|
||||
|
||||
import { describe, it, beforeAll, expect } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { join } from 'node:path';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
// Create a minimal MCP server that doesn't require external dependencies
|
||||
// This implements the MCP protocol directly using Node.js built-ins
|
||||
@@ -180,15 +192,14 @@ describe('mcp server with cyclic tool schema is detected', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should error and suggest disabling the cyclic tool', async () => {
|
||||
// Just run any command to trigger the schema depth error.
|
||||
// If this test starts failing, check `isSchemaDepthError` from
|
||||
// geminiChat.ts to see if it needs to be updated.
|
||||
// Or, possibly it could mean that gemini has fixed the issue.
|
||||
const output = await rig.run('hello');
|
||||
it('mcp tool list should include tool with cyclic tool schema', async () => {
|
||||
const tool_list_output = await rig.run('/mcp list');
|
||||
expect(tool_list_output).toContain('tool_with_cyclic_schema');
|
||||
});
|
||||
|
||||
expect(output).toMatch(
|
||||
/Skipping tool 'tool_with_cyclic_schema' from MCP server 'cyclic-schema-server' because it has missing types in its parameter schema/,
|
||||
);
|
||||
it('gemini api call should be successful with cyclic mcp tool schema', async () => {
|
||||
// Run any command and verify that we get a non-error response from
|
||||
// the Gemini API.
|
||||
await rig.run('hello');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('mixed input crash prevention', () => {
|
||||
it('should not crash when using mixed prompt inputs', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('should not crash when using mixed prompt inputs');
|
||||
|
||||
// Test: echo "say '1'." | gemini --prompt-interactive="say '2'." say '3'.
|
||||
const stdinContent = "say '1'.";
|
||||
|
||||
try {
|
||||
await rig.run(
|
||||
{ stdin: stdinContent },
|
||||
'--prompt-interactive',
|
||||
"say '2'.",
|
||||
"say '3'.",
|
||||
);
|
||||
throw new Error('Expected the command to fail, but it succeeded');
|
||||
} catch (error: unknown) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
const err = error as Error;
|
||||
|
||||
expect(err.message).toContain('Process exited with code 1');
|
||||
expect(err.message).toContain(
|
||||
'--prompt-interactive flag cannot be used when input is piped',
|
||||
);
|
||||
expect(err.message).not.toContain('setRawMode is not a function');
|
||||
expect(err.message).not.toContain('unexpected critical error');
|
||||
}
|
||||
|
||||
const lastRequest = rig.readLastApiRequest();
|
||||
expect(lastRequest).toBeNull();
|
||||
});
|
||||
|
||||
it('should provide clear error message for mixed input', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('should provide clear error message for mixed input');
|
||||
|
||||
try {
|
||||
await rig.run(
|
||||
{ stdin: 'test input' },
|
||||
'--prompt-interactive',
|
||||
'test prompt',
|
||||
);
|
||||
throw new Error('Expected the command to fail, but it succeeded');
|
||||
} catch (error: unknown) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
const err = error as Error;
|
||||
|
||||
expect(err.message).toContain(
|
||||
'--prompt-interactive flag cannot be used when input is piped',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,13 +8,13 @@ import { describe, it, expect } from 'vitest';
|
||||
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||
|
||||
describe('read_many_files', () => {
|
||||
it('should be able to read multiple files', async () => {
|
||||
it.skip('should be able to read multiple files', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should be able to read multiple files');
|
||||
rig.createFile('file1.txt', 'file 1 content');
|
||||
rig.createFile('file2.txt', 'file 2 content');
|
||||
|
||||
const prompt = `Please use read_many_files to read file1.txt and file2.txt and show me what's in them`;
|
||||
const prompt = `Use the read_many_files tool to read the contents of file1.txt and file2.txt and then print the contents of each file.`;
|
||||
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
@@ -41,11 +41,7 @@ describe('read_many_files', () => {
|
||||
'Expected to find either read_many_files or multiple read_file tool calls',
|
||||
).toBeTruthy();
|
||||
|
||||
// Validate model output - will throw if no output, warn if missing expected content
|
||||
validateModelOutput(
|
||||
result,
|
||||
['file 1 content', 'file 2 content'],
|
||||
'Read many files test',
|
||||
);
|
||||
// Validate model output - will throw if no output
|
||||
validateModelOutput(result, null, 'Read many files test');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,4 +61,34 @@ describe('replace', () => {
|
||||
console.log('File replaced successfully. New content:', newFileContent);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle $ literally when replacing text ending with $', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup(
|
||||
'should handle $ literally when replacing text ending with $',
|
||||
);
|
||||
|
||||
const fileName = 'regex.yml';
|
||||
const originalContent = "| select('match', '^[sv]d[a-z]$')\n";
|
||||
const expectedContent = "| select('match', '^[sv]d[a-z]$') # updated\n";
|
||||
|
||||
rig.createFile(fileName, originalContent);
|
||||
|
||||
const prompt =
|
||||
"Open regex.yml and append ' # updated' after the line containing ^[sv]d[a-z]$ without breaking the $ character.";
|
||||
|
||||
const result = await rig.run(prompt);
|
||||
const foundToolCall = await rig.waitForToolCall('replace');
|
||||
|
||||
if (!foundToolCall) {
|
||||
printDebugInfo(rig, result);
|
||||
}
|
||||
|
||||
expect(foundToolCall, 'Expected to find a replace tool call').toBeTruthy();
|
||||
|
||||
validateModelOutput(result, ['regex.yml'], 'Replace $ literal test');
|
||||
|
||||
const newFileContent = rig.readFile(fileName);
|
||||
expect(newFileContent).toBe(expectedContent);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,4 +67,64 @@ describe('run_shell_command', () => {
|
||||
// Validate model output - will throw if no output, warn if missing expected content
|
||||
validateModelOutput(result, 'test-stdin', 'Shell command stdin test');
|
||||
});
|
||||
|
||||
it('should propagate environment variables to the child process', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should propagate environment variables');
|
||||
|
||||
const varName = 'GEMINI_CLI_TEST_VAR';
|
||||
const varValue = `test-value-${Math.random().toString(36).substring(7)}`;
|
||||
process.env[varName] = varValue;
|
||||
|
||||
try {
|
||||
const prompt = `Use echo to learn the value of the environment variable named ${varName} and tell me what it is.`;
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command');
|
||||
|
||||
if (!foundToolCall || !result.includes(varValue)) {
|
||||
printDebugInfo(rig, result, {
|
||||
'Found tool call': foundToolCall,
|
||||
'Contains varValue': result.includes(varValue),
|
||||
});
|
||||
}
|
||||
|
||||
expect(
|
||||
foundToolCall,
|
||||
'Expected to find a run_shell_command tool call',
|
||||
).toBeTruthy();
|
||||
validateModelOutput(result, varValue, 'Env var propagation test');
|
||||
expect(result).toContain(varValue);
|
||||
} finally {
|
||||
delete process.env[varName];
|
||||
}
|
||||
});
|
||||
|
||||
it('should run a platform-specific file listing command', async () => {
|
||||
const rig = new TestRig();
|
||||
await rig.setup('should run platform-specific file listing');
|
||||
const fileName = `test-file-${Math.random().toString(36).substring(7)}.txt`;
|
||||
rig.createFile(fileName, 'test content');
|
||||
|
||||
const prompt = `Run a shell command to list the files in the current directory and tell me what they are.`;
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('run_shell_command');
|
||||
|
||||
// Debugging info
|
||||
if (!foundToolCall || !result.includes(fileName)) {
|
||||
printDebugInfo(rig, result, {
|
||||
'Found tool call': foundToolCall,
|
||||
'Contains fileName': result.includes(fileName),
|
||||
});
|
||||
}
|
||||
|
||||
expect(
|
||||
foundToolCall,
|
||||
'Expected to find a run_shell_command tool call',
|
||||
).toBeTruthy();
|
||||
|
||||
validateModelOutput(result, fileName, 'Platform-specific listing test');
|
||||
expect(result).toContain(fileName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { ShellExecutionService } from '../packages/core/src/services/shellExecutionService.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('ShellExecutionService programmatic integration tests', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a dedicated directory for this test suite to avoid conflicts.
|
||||
testDir = path.join(
|
||||
process.env['INTEGRATION_TEST_FILE_DIR']!,
|
||||
'shell-service-tests',
|
||||
);
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
it('should execute a simple cross-platform command (echo)', async () => {
|
||||
const command = 'echo "hello from the service"';
|
||||
const onOutputEvent = vi.fn();
|
||||
const abortController = new AbortController();
|
||||
|
||||
const handle = await ShellExecutionService.execute(
|
||||
command,
|
||||
testDir,
|
||||
onOutputEvent,
|
||||
abortController.signal,
|
||||
false,
|
||||
);
|
||||
|
||||
const result = await handle.result;
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.exitCode).toBe(0);
|
||||
// Output can vary slightly between shells (e.g., quotes), so check for inclusion.
|
||||
expect(result.output).toContain('hello from the service');
|
||||
});
|
||||
|
||||
it.runIf(process.platform === 'win32')(
|
||||
'should execute "dir" on Windows',
|
||||
async () => {
|
||||
const testFile = 'test-file-windows.txt';
|
||||
await fs.writeFile(path.join(testDir, testFile), 'windows test');
|
||||
|
||||
const command = 'dir';
|
||||
const onOutputEvent = vi.fn();
|
||||
const abortController = new AbortController();
|
||||
|
||||
const handle = await ShellExecutionService.execute(
|
||||
command,
|
||||
testDir,
|
||||
onOutputEvent,
|
||||
abortController.signal,
|
||||
false,
|
||||
);
|
||||
|
||||
const result = await handle.result;
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toContain(testFile);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'should execute "ls -l" on Unix',
|
||||
async () => {
|
||||
const testFile = 'test-file-unix.txt';
|
||||
await fs.writeFile(path.join(testDir, testFile), 'unix test');
|
||||
|
||||
const command = 'ls -l';
|
||||
const onOutputEvent = vi.fn();
|
||||
const abortController = new AbortController();
|
||||
|
||||
const handle = await ShellExecutionService.execute(
|
||||
command,
|
||||
testDir,
|
||||
onOutputEvent,
|
||||
abortController.signal,
|
||||
false,
|
||||
);
|
||||
|
||||
const result = await handle.result;
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toContain(testFile);
|
||||
},
|
||||
);
|
||||
|
||||
it('should abort a running process', async () => {
|
||||
// A command that runs for a bit. 'sleep' on unix, 'timeout' on windows.
|
||||
const command = process.platform === 'win32' ? 'timeout /t 20' : 'sleep 20';
|
||||
const onOutputEvent = vi.fn();
|
||||
const abortController = new AbortController();
|
||||
|
||||
const handle = await ShellExecutionService.execute(
|
||||
command,
|
||||
testDir,
|
||||
onOutputEvent,
|
||||
abortController.signal,
|
||||
false,
|
||||
);
|
||||
|
||||
// Abort shortly after starting
|
||||
setTimeout(() => abortController.abort(), 50);
|
||||
|
||||
const result = await handle.result;
|
||||
|
||||
// For debugging the flaky test.
|
||||
console.log('Abort test result:', result);
|
||||
|
||||
expect(result.aborted).toBe(true);
|
||||
// A clean exit is exitCode 0 and no signal. If the process was truly
|
||||
// aborted, it should not have exited cleanly.
|
||||
const exitedCleanly = result.exitCode === 0 && result.signal === null;
|
||||
expect(exitedCleanly, 'Process should not have exited cleanly').toBe(false);
|
||||
});
|
||||
|
||||
it('should propagate environment variables to the child process', async () => {
|
||||
const varName = 'GEMINI_CLI_TEST_VAR';
|
||||
const varValue = `test-value`;
|
||||
process.env[varName] = varValue;
|
||||
|
||||
try {
|
||||
const command =
|
||||
process.platform === 'win32' ? `echo %${varName}%` : `echo $${varName}`;
|
||||
const onOutputEvent = vi.fn();
|
||||
const abortController = new AbortController();
|
||||
|
||||
const handle = await ShellExecutionService.execute(
|
||||
command,
|
||||
testDir,
|
||||
onOutputEvent,
|
||||
abortController.signal,
|
||||
false,
|
||||
);
|
||||
|
||||
const result = await handle.result;
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toContain(varValue);
|
||||
} finally {
|
||||
// Clean up the env var to prevent side-effects on other tests.
|
||||
delete process.env[varName];
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -189,6 +189,25 @@ describe('simple-mcp-server', () => {
|
||||
const { chmodSync } = await import('node:fs');
|
||||
chmodSync(testServerPath, 0o755);
|
||||
}
|
||||
|
||||
// Poll for script for up to 5s
|
||||
const { accessSync, constants } = await import('node:fs');
|
||||
const isReady = await rig.poll(
|
||||
() => {
|
||||
try {
|
||||
accessSync(testServerPath, constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
5000, // Max wait 5 seconds
|
||||
100, // Poll every 100ms
|
||||
);
|
||||
|
||||
if (!isReady) {
|
||||
throw new Error('MCP server script was not ready in time.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should add two numbers', async () => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('telemetry', () => {
|
||||
it('should emit a metric and a log event', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('should emit a metric and a log event');
|
||||
|
||||
// Run a simple command that should trigger telemetry
|
||||
await rig.run('just saying hi');
|
||||
|
||||
// Verify that a user_prompt event was logged
|
||||
const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
|
||||
expect(hasUserPromptEvent).toBe(true);
|
||||
|
||||
// Verify that a cli_command_count metric was emitted
|
||||
const cliCommandCountMetric = rig.readMetric('session.count');
|
||||
expect(cliCommandCountMetric).not.toBeNull();
|
||||
});
|
||||
});
|
||||
+201
-121
@@ -5,12 +5,13 @@
|
||||
*/
|
||||
|
||||
import { execSync, spawn } from 'node:child_process';
|
||||
import { parse } from 'shell-quote';
|
||||
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { env } from 'node:process';
|
||||
import { DEFAULT_GEMINI_MODEL } from '../packages/core/src/config/models.js';
|
||||
import fs from 'node:fs';
|
||||
import * as pty from '@lydell/node-pty';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -111,6 +112,23 @@ export function validateModelOutput(
|
||||
return true;
|
||||
}
|
||||
|
||||
interface ParsedLog {
|
||||
attributes?: {
|
||||
'event.name'?: string;
|
||||
function_name?: string;
|
||||
function_args?: string;
|
||||
success?: boolean;
|
||||
duration_ms?: number;
|
||||
};
|
||||
scopeMetrics?: {
|
||||
metrics: {
|
||||
descriptor: {
|
||||
name: string;
|
||||
};
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export class TestRig {
|
||||
bundlePath: string;
|
||||
testDir: string | null;
|
||||
@@ -152,6 +170,7 @@ export class TestRig {
|
||||
otlpEndpoint: '',
|
||||
outfile: telemetryPath,
|
||||
},
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
sandbox: env.GEMINI_SANDBOX !== 'false' ? env.GEMINI_SANDBOX : false,
|
||||
...options.settings, // Allow tests to override/add settings
|
||||
};
|
||||
@@ -182,7 +201,7 @@ export class TestRig {
|
||||
| { prompt?: string; stdin?: string; stdinDoesNotEnd?: boolean },
|
||||
...args: string[]
|
||||
): Promise<string> {
|
||||
let command = `node ${this.bundlePath} --yolo`;
|
||||
const commandArgs = [this.bundlePath, '--yolo'];
|
||||
const execOptions: {
|
||||
cwd: string;
|
||||
encoding: 'utf-8';
|
||||
@@ -193,27 +212,25 @@ export class TestRig {
|
||||
};
|
||||
|
||||
if (typeof promptOrOptions === 'string') {
|
||||
command += ` --prompt ${JSON.stringify(promptOrOptions)}`;
|
||||
commandArgs.push('--prompt', promptOrOptions);
|
||||
} else if (
|
||||
typeof promptOrOptions === 'object' &&
|
||||
promptOrOptions !== null
|
||||
) {
|
||||
if (promptOrOptions.prompt) {
|
||||
command += ` --prompt ${JSON.stringify(promptOrOptions.prompt)}`;
|
||||
commandArgs.push('--prompt', promptOrOptions.prompt);
|
||||
}
|
||||
if (promptOrOptions.stdin) {
|
||||
execOptions.input = promptOrOptions.stdin;
|
||||
}
|
||||
}
|
||||
|
||||
command += ` ${args.join(' ')}`;
|
||||
commandArgs.push(...args);
|
||||
|
||||
const commandArgs = parse(command);
|
||||
const node = commandArgs.shift() as string;
|
||||
|
||||
const child = spawn(node, commandArgs as string[], {
|
||||
const child = spawn('node', commandArgs, {
|
||||
cwd: this.testDir!,
|
||||
stdio: 'pipe',
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
@@ -288,8 +305,15 @@ export class TestRig {
|
||||
|
||||
result = filteredLines.join('\n');
|
||||
}
|
||||
// If we have stderr output, include that also
|
||||
if (stderr) {
|
||||
|
||||
// Check if this is a JSON output test - if so, don't include stderr
|
||||
// as it would corrupt the JSON
|
||||
const isJsonOutput =
|
||||
commandArgs.includes('--output-format') &&
|
||||
commandArgs.includes('json');
|
||||
|
||||
// If we have stderr output and it's not a JSON test, include that also
|
||||
if (stderr && !isJsonOutput) {
|
||||
result += `\n\nStdErr:\n${stderr}`;
|
||||
}
|
||||
|
||||
@@ -303,6 +327,57 @@ export class TestRig {
|
||||
return promise;
|
||||
}
|
||||
|
||||
runCommand(
|
||||
args: string[],
|
||||
options: { stdin?: string } = {},
|
||||
): Promise<string> {
|
||||
const commandArgs = [this.bundlePath, ...args];
|
||||
|
||||
const child = spawn('node', commandArgs, {
|
||||
cwd: this.testDir!,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
if (options.stdin) {
|
||||
child.stdin!.write(options.stdin);
|
||||
child.stdin!.end();
|
||||
}
|
||||
|
||||
child.stdout!.on('data', (data: Buffer) => {
|
||||
stdout += data;
|
||||
if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') {
|
||||
process.stdout.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr!.on('data', (data: Buffer) => {
|
||||
stderr += data;
|
||||
if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') {
|
||||
process.stderr.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
const promise = new Promise<string>((resolve, reject) => {
|
||||
child.on('close', (code: number) => {
|
||||
if (code === 0) {
|
||||
this._lastRunStdout = stdout;
|
||||
let result = stdout;
|
||||
if (stderr) {
|
||||
result += `\n\nStdErr:\n${stderr}`;
|
||||
}
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(new Error(`Process exited with code ${code}:\n${stderr}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
readFile(fileName: string) {
|
||||
const filePath = join(this.testDir!, fileName);
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
@@ -360,37 +435,12 @@ export class TestRig {
|
||||
|
||||
return this.poll(
|
||||
() => {
|
||||
const logFilePath = join(this.testDir!, 'telemetry.log');
|
||||
|
||||
if (!logFilePath || !fs.existsSync(logFilePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = readFileSync(logFilePath, 'utf-8');
|
||||
const jsonObjects = content
|
||||
.split(/}\n{/)
|
||||
.map((obj, index, array) => {
|
||||
// Add back the braces we removed during split
|
||||
if (index > 0) obj = '{' + obj;
|
||||
if (index < array.length - 1) obj = obj + '}';
|
||||
return obj.trim();
|
||||
})
|
||||
.filter((obj) => obj);
|
||||
|
||||
for (const jsonStr of jsonObjects) {
|
||||
try {
|
||||
const logData = JSON.parse(jsonStr);
|
||||
if (
|
||||
logData.attributes &&
|
||||
logData.attributes['event.name'] === `gemini_cli.${eventName}`
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
const logs = this._readAndParseTelemetryLog();
|
||||
return logs.some(
|
||||
(logData) =>
|
||||
logData.attributes &&
|
||||
logData.attributes['event.name'] === `gemini_cli.${eventName}`,
|
||||
);
|
||||
},
|
||||
timeout,
|
||||
100,
|
||||
@@ -587,6 +637,45 @@ export class TestRig {
|
||||
return logs;
|
||||
}
|
||||
|
||||
private _readAndParseTelemetryLog(): ParsedLog[] {
|
||||
// Telemetry is always written to the test directory
|
||||
const logFilePath = join(this.testDir!, 'telemetry.log');
|
||||
|
||||
if (!logFilePath || !fs.existsSync(logFilePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = readFileSync(logFilePath, 'utf-8');
|
||||
|
||||
// Split the content into individual JSON objects
|
||||
// They are separated by "}\n{"
|
||||
const jsonObjects = content
|
||||
.split(/}\n{/)
|
||||
.map((obj, index, array) => {
|
||||
// Add back the braces we removed during split
|
||||
if (index > 0) obj = '{' + obj;
|
||||
if (index < array.length - 1) obj = obj + '}';
|
||||
return obj.trim();
|
||||
})
|
||||
.filter((obj) => obj);
|
||||
|
||||
const logs: ParsedLog[] = [];
|
||||
|
||||
for (const jsonStr of jsonObjects) {
|
||||
try {
|
||||
const logData = JSON.parse(jsonStr);
|
||||
logs.push(logData);
|
||||
} catch (e) {
|
||||
// Skip objects that aren't valid JSON
|
||||
if (env.VERBOSE === 'true') {
|
||||
console.error('Failed to parse telemetry object:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
readToolLogs() {
|
||||
// For Podman, first check if telemetry file exists and has content
|
||||
// If not, fall back to parsing from stdout
|
||||
@@ -616,33 +705,7 @@ export class TestRig {
|
||||
}
|
||||
}
|
||||
|
||||
// Telemetry is always written to the test directory
|
||||
const logFilePath = join(this.testDir!, 'telemetry.log');
|
||||
|
||||
if (!logFilePath) {
|
||||
console.warn(`TELEMETRY_LOG_FILE environment variable not set`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if file exists, if not return empty array (file might not be created yet)
|
||||
if (!fs.existsSync(logFilePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = readFileSync(logFilePath, 'utf-8');
|
||||
|
||||
// Split the content into individual JSON objects
|
||||
// They are separated by "}\n{"
|
||||
const jsonObjects = content
|
||||
.split(/}\n{/)
|
||||
.map((obj, index, array) => {
|
||||
// Add back the braces we removed during split
|
||||
if (index > 0) obj = '{' + obj;
|
||||
if (index < array.length - 1) obj = obj + '}';
|
||||
return obj.trim();
|
||||
})
|
||||
.filter((obj) => obj);
|
||||
|
||||
const parsedLogs = this._readAndParseTelemetryLog();
|
||||
const logs: {
|
||||
toolRequest: {
|
||||
name: string;
|
||||
@@ -652,29 +715,21 @@ export class TestRig {
|
||||
};
|
||||
}[] = [];
|
||||
|
||||
for (const jsonStr of jsonObjects) {
|
||||
try {
|
||||
const logData = JSON.parse(jsonStr);
|
||||
// Look for tool call logs
|
||||
if (
|
||||
logData.attributes &&
|
||||
logData.attributes['event.name'] === 'gemini_cli.tool_call'
|
||||
) {
|
||||
const toolName = logData.attributes.function_name;
|
||||
logs.push({
|
||||
toolRequest: {
|
||||
name: toolName,
|
||||
args: logData.attributes.function_args,
|
||||
success: logData.attributes.success,
|
||||
duration_ms: logData.attributes.duration_ms,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip objects that aren't valid JSON
|
||||
if (env.VERBOSE === 'true') {
|
||||
console.error('Failed to parse telemetry object:', e);
|
||||
}
|
||||
for (const logData of parsedLogs) {
|
||||
// Look for tool call logs
|
||||
if (
|
||||
logData.attributes &&
|
||||
logData.attributes['event.name'] === 'gemini_cli.tool_call'
|
||||
) {
|
||||
const toolName = logData.attributes.function_name;
|
||||
logs.push({
|
||||
toolRequest: {
|
||||
name: toolName,
|
||||
args: logData.attributes.function_args,
|
||||
success: logData.attributes.success,
|
||||
duration_ms: logData.attributes.duration_ms,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,38 +737,63 @@ export class TestRig {
|
||||
}
|
||||
|
||||
readLastApiRequest(): Record<string, unknown> | null {
|
||||
// Telemetry is always written to the test directory
|
||||
const logFilePath = join(this.testDir!, 'telemetry.log');
|
||||
const logs = this._readAndParseTelemetryLog();
|
||||
const apiRequests = logs.filter(
|
||||
(logData) =>
|
||||
logData.attributes &&
|
||||
logData.attributes['event.name'] === 'gemini_cli.api_request',
|
||||
);
|
||||
return apiRequests.pop() || null;
|
||||
}
|
||||
|
||||
if (!logFilePath || !fs.existsSync(logFilePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = readFileSync(logFilePath, 'utf-8');
|
||||
const jsonObjects = content
|
||||
.split(/}\n{/)
|
||||
.map((obj, index, array) => {
|
||||
if (index > 0) obj = '{' + obj;
|
||||
if (index < array.length - 1) obj = obj + '}';
|
||||
return obj.trim();
|
||||
})
|
||||
.filter((obj) => obj);
|
||||
|
||||
let lastApiRequest = null;
|
||||
|
||||
for (const jsonStr of jsonObjects) {
|
||||
try {
|
||||
const logData = JSON.parse(jsonStr);
|
||||
if (
|
||||
logData.attributes &&
|
||||
logData.attributes['event.name'] === 'gemini_cli.api_request'
|
||||
) {
|
||||
lastApiRequest = logData;
|
||||
readMetric(metricName: string): Record<string, unknown> | null {
|
||||
const logs = this._readAndParseTelemetryLog();
|
||||
for (const logData of logs) {
|
||||
if (logData.scopeMetrics) {
|
||||
for (const scopeMetric of logData.scopeMetrics) {
|
||||
for (const metric of scopeMetric.metrics) {
|
||||
if (metric.descriptor.name === `gemini_cli.${metricName}`) {
|
||||
return metric;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return lastApiRequest;
|
||||
return null;
|
||||
}
|
||||
|
||||
runInteractive(...args: string[]): {
|
||||
ptyProcess: pty.IPty;
|
||||
promise: Promise<{ exitCode: number; signal?: number; output: string }>;
|
||||
} {
|
||||
const commandArgs = [this.bundlePath, '--yolo', ...args];
|
||||
|
||||
const ptyProcess = pty.spawn('node', commandArgs, {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
cwd: this.testDir!,
|
||||
env: process.env as { [key: string]: string },
|
||||
});
|
||||
|
||||
let output = '';
|
||||
ptyProcess.onData((data) => {
|
||||
output += data;
|
||||
if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') {
|
||||
process.stdout.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
const promise = new Promise<{
|
||||
exitCode: number;
|
||||
signal?: number;
|
||||
output: string;
|
||||
}>((resolve) => {
|
||||
ptyProcess.onExit(({ exitCode, signal }) => {
|
||||
resolve({ exitCode, signal, output });
|
||||
});
|
||||
});
|
||||
|
||||
return { ptyProcess, promise };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { writeFileSync, readFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
// Windows skip (Option A: avoid infra scope)
|
||||
const d = process.platform === 'win32' ? describe.skip : describe;
|
||||
|
||||
// BOM encoders
|
||||
const utf8BOM = (s: string) =>
|
||||
Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(s, 'utf8')]);
|
||||
const utf16LE = (s: string) =>
|
||||
Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(s, 'utf16le')]);
|
||||
const utf16BE = (s: string) => {
|
||||
const bom = Buffer.from([0xfe, 0xff]);
|
||||
const le = Buffer.from(s, 'utf16le');
|
||||
le.swap16();
|
||||
return Buffer.concat([bom, le]);
|
||||
};
|
||||
const utf32LE = (s: string) => {
|
||||
const bom = Buffer.from([0xff, 0xfe, 0x00, 0x00]);
|
||||
const cps = Array.from(s, (c) => c.codePointAt(0)!);
|
||||
const payload = Buffer.alloc(cps.length * 4);
|
||||
cps.forEach((cp, i) => {
|
||||
const o = i * 4;
|
||||
payload[o] = cp & 0xff;
|
||||
payload[o + 1] = (cp >>> 8) & 0xff;
|
||||
payload[o + 2] = (cp >>> 16) & 0xff;
|
||||
payload[o + 3] = (cp >>> 24) & 0xff;
|
||||
});
|
||||
return Buffer.concat([bom, payload]);
|
||||
};
|
||||
const utf32BE = (s: string) => {
|
||||
const bom = Buffer.from([0x00, 0x00, 0xfe, 0xff]);
|
||||
const cps = Array.from(s, (c) => c.codePointAt(0)!);
|
||||
const payload = Buffer.alloc(cps.length * 4);
|
||||
cps.forEach((cp, i) => {
|
||||
const o = i * 4;
|
||||
payload[o] = (cp >>> 24) & 0xff;
|
||||
payload[o + 1] = (cp >>> 16) & 0xff;
|
||||
payload[o + 2] = (cp >>> 8) & 0xff;
|
||||
payload[o + 3] = cp & 0xff;
|
||||
});
|
||||
return Buffer.concat([bom, payload]);
|
||||
};
|
||||
|
||||
let rig: TestRig;
|
||||
let dir: string;
|
||||
|
||||
d('BOM end-to-end integration', () => {
|
||||
beforeAll(async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('bom-integration');
|
||||
dir = rig.testDir!;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
async function runAndAssert(
|
||||
filename: string,
|
||||
content: Buffer,
|
||||
expectedText: string | null,
|
||||
) {
|
||||
writeFileSync(join(dir, filename), content);
|
||||
const prompt = `read the file ${filename} and output its exact contents`;
|
||||
const output = await rig.run(prompt);
|
||||
await rig.waitForToolCall('read_file');
|
||||
const lower = output.toLowerCase();
|
||||
if (expectedText === null) {
|
||||
expect(
|
||||
lower.includes('binary') ||
|
||||
lower.includes('skipped binary file') ||
|
||||
lower.includes('cannot display'),
|
||||
).toBeTruthy();
|
||||
} else {
|
||||
expect(output.includes(expectedText)).toBeTruthy();
|
||||
expect(lower.includes('skipped binary file')).toBeFalsy();
|
||||
}
|
||||
}
|
||||
|
||||
it('UTF-8 BOM', async () => {
|
||||
await runAndAssert('utf8.txt', utf8BOM('BOM_OK UTF-8'), 'BOM_OK UTF-8');
|
||||
});
|
||||
|
||||
it('UTF-16 LE BOM', async () => {
|
||||
await runAndAssert(
|
||||
'utf16le.txt',
|
||||
utf16LE('BOM_OK UTF-16LE'),
|
||||
'BOM_OK UTF-16LE',
|
||||
);
|
||||
});
|
||||
|
||||
it('UTF-16 BE BOM', async () => {
|
||||
await runAndAssert(
|
||||
'utf16be.txt',
|
||||
utf16BE('BOM_OK UTF-16BE'),
|
||||
'BOM_OK UTF-16BE',
|
||||
);
|
||||
});
|
||||
|
||||
it('UTF-32 LE BOM', async () => {
|
||||
await runAndAssert(
|
||||
'utf32le.txt',
|
||||
utf32LE('BOM_OK UTF-32LE'),
|
||||
'BOM_OK UTF-32LE',
|
||||
);
|
||||
});
|
||||
|
||||
it('UTF-32 BE BOM', async () => {
|
||||
await runAndAssert(
|
||||
'utf32be.txt',
|
||||
utf32BE('BOM_OK UTF-32BE'),
|
||||
'BOM_OK UTF-32BE',
|
||||
);
|
||||
});
|
||||
|
||||
it('Can describe a PNG file', async () => {
|
||||
const imagePath = resolve(
|
||||
process.cwd(),
|
||||
'docs/assets/gemini-screenshot.png',
|
||||
);
|
||||
const imageContent = readFileSync(imagePath);
|
||||
const filename = 'gemini-screenshot.png';
|
||||
writeFileSync(join(dir, filename), imageContent);
|
||||
const prompt = `What is shown in the image ${filename}?`;
|
||||
const output = await rig.run(prompt);
|
||||
await rig.waitForToolCall('read_file');
|
||||
const lower = output.toLowerCase();
|
||||
// The response is non-deterministic, so we just check for some
|
||||
// keywords that are very likely to be in the response.
|
||||
expect(lower.includes('gemini')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,12 @@ export default defineConfig({
|
||||
reporters: ['default'],
|
||||
include: ['**/*.test.ts'],
|
||||
retry: 2,
|
||||
fileParallelism: false,
|
||||
fileParallelism: true,
|
||||
poolOptions: {
|
||||
threads: {
|
||||
minThreads: 8,
|
||||
maxThreads: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+3186
-694
File diff suppressed because it is too large
Load Diff
+38
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.2.1",
|
||||
"version": "0.8.0-nightly.20250930.ddcbd0c2",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,10 +14,10 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.2.1"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.8.0-nightly.20250930.ddcbd0c2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/start.js",
|
||||
"start": "cross-env node scripts/start.js",
|
||||
"start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server",
|
||||
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
|
||||
"auth:npm": "npx google-artifactregistry-auth",
|
||||
@@ -29,27 +29,37 @@
|
||||
"build:vscode": "node scripts/build_vscode_companion.js",
|
||||
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
|
||||
"build:packages": "npm run build --workspaces",
|
||||
"build:sandbox": "node scripts/build_sandbox.js --skip-npm-install-build",
|
||||
"build:sandbox": "node scripts/build_sandbox.js",
|
||||
"bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
|
||||
"test": "npm run test --workspaces --if-present",
|
||||
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts",
|
||||
"test": "npm run test --workspaces --if-present --parallel",
|
||||
"test:ci": "npm run test:ci --workspaces --if-present --parallel && npm run test:scripts",
|
||||
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
|
||||
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
|
||||
"test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman",
|
||||
"test:integration:sandbox:none": "GEMINI_SANDBOX=false vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:docker": "GEMINI_SANDBOX=docker npm run build:sandbox && GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:podman": "GEMINI_SANDBOX=podman vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests",
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
|
||||
"lint:fix": "eslint . --fix && eslint integration-tests --fix",
|
||||
"lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0",
|
||||
"lint:all": "node scripts/lint.js",
|
||||
"format": "prettier --experimental-cli --write .",
|
||||
"typecheck": "npm run typecheck --workspaces --if-present",
|
||||
"preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
|
||||
"prepare": "npm run bundle",
|
||||
"prepare": "husky && npm run bundle",
|
||||
"prepare:package": "node scripts/prepare-package.js",
|
||||
"release:version": "node scripts/version.js",
|
||||
"telemetry": "node scripts/telemetry.js",
|
||||
"clean": "node scripts/clean.js"
|
||||
"check:lockfile": "node scripts/check-lockfile.js",
|
||||
"clean": "node scripts/clean.js",
|
||||
"pre-commit": "node scripts/pre-commit.js"
|
||||
},
|
||||
"overrides": {
|
||||
"wrap-ansi": "9.0.2",
|
||||
"ansi-regex": "5.0.1",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"gemini": "bundle/gemini.js"
|
||||
@@ -67,7 +77,6 @@
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/eslint-plugin": "^1.3.4",
|
||||
"concurrently": "^9.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.24.0",
|
||||
@@ -78,23 +87,26 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"glob": "^10.4.5",
|
||||
"globals": "^16.0.0",
|
||||
"google-artifactregistry-auth": "^3.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"json": "^11.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"memfs": "^4.17.2",
|
||||
"lint-staged": "^16.1.6",
|
||||
"memfs": "^4.42.0",
|
||||
"mnemonist": "^0.40.3",
|
||||
"mock-fs": "^5.5.0",
|
||||
"msw": "^2.10.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.5.3",
|
||||
"react-devtools-core": "^4.28.5",
|
||||
"semver": "^7.7.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vitest": "^3.2.4",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lvce-editor/ripgrep": "^1.6.0",
|
||||
"simple-git": "^3.28.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"simple-git": "^3.28.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
@@ -104,5 +116,14 @@
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0",
|
||||
"node-pty": "^1.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix --max-warnings 0"
|
||||
],
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
# RFC: Gemini CLI A2A Development-Tool Extension
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### 1.1 Overview
|
||||
|
||||
To standardize client integrations with the Gemini CLI agent, this document proposes the `development-tool` extension for the A2A protocol.
|
||||
|
||||
Rather than creating a new protocol, this specification builds upon the existing A2A protocol. As an open-source standard recently adopted by the Linux Foundation, A2A provides a robust foundation for core concepts like tasks, messages, and streaming events. This extension-based approach allows us to leverage A2A's proven architecture while defining the specific capabilities required for rich, interactive workflows with the Gemini CLI agent.
|
||||
|
||||
### 1.2 Motivation
|
||||
|
||||
Recent work integrating Gemini CLI with clients like Zed and Gemini Code Assist’s agent mode has highlighted the need for a robust, standard communication protocol. Standardizing on A2A provides several key advantages:
|
||||
|
||||
- **Solid Foundation**: Provides a robust, open standard that ensures a stable, predictable, and consistent integration experience across different IDEs and client surfaces.
|
||||
- **Extensibility**: Creates a flexible foundation to support new tools and workflows as they emerge.
|
||||
- **Ecosystem Alignment**: Aligns Gemini CLI with a growing industry standard, fostering broader interoperability.
|
||||
|
||||
## 2. Communication Flow
|
||||
|
||||
The interaction follows A2A’s task-based, streaming pattern. The client sends a `message/stream` request and the agent responds with a `contextId` / `taskId` and a stream of events. `TaskStatusUpdateEvent` events are used to convey the overall state of the task. The task is complete when the agent sends a final `TaskStatusUpdateEvent` with `final: true` and a terminal status like `completed` or `failed`.
|
||||
|
||||
### 2.1 Asynchronous Responses and Notifications
|
||||
|
||||
Clients that may disconnect from the agent should supply a `PushNotificationConfig` to the agent with the initial `message/stream` method or subsequently with the `tasks/pushNotificationConfig/set` method so that the agent can call back when updates are ready.
|
||||
|
||||
## 3. The `development-tool` extension
|
||||
|
||||
### 3.1 Overview
|
||||
|
||||
The `development-tool` extension establishes a communication contract for workflows between a client and the Gemini CLI agent. It consists of a specialized set of schemas, embedded within core A2A data structures, that enable the agent to stream real-time updates on its state and thought process. These schemas also provide the mechanism for the agent to request user permission before executing tools.
|
||||
|
||||
**Sample Agent Card**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Gemini CLI Agent",
|
||||
"description": "An agent that generates code based on natural language instructions.",
|
||||
"capabilities": {
|
||||
"streaming": true,
|
||||
"extensions": [
|
||||
{
|
||||
"uri": "https://github.com/google-gemini/gemini-cli/blob/main/docs/a2a/developer-profile/v0/spec.md",
|
||||
"description": "An extension for interactive development tasks, enabling features like code generation, tool usage, and real-time status updates.",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Versioning**
|
||||
|
||||
The agent card `uri` field contains an embedded semantic version. The client must extract this version to determine compatibility with the agent extension using the compatibility logic defined in Semantic Versioning 2.0.0 spec.
|
||||
|
||||
### 3.2 Schema Definitions
|
||||
|
||||
This section defines the schemas for the `development-tool` A2A extension, organized by their function within the communication flow. Note that all custom objects included in the `metadata` field (e.g. `Message.metadata`) must be keyed by the unique URI that points to that extension’s spec to prevent naming collisions with other extensions.
|
||||
|
||||
**Initialization & Configuration**
|
||||
|
||||
The first message in a session must contain an `AgentSettings` object in its metadata. This object provides the agent with the necessary configuration information for proper initialization. Additional configuration settings (ex. MCP servers, allowed tools, etc.) can be added to this message.
|
||||
|
||||
**Schema**
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
// Configuration settings for the Gemini CLI agent.
|
||||
message AgentSettings {
|
||||
// The absolute path to the workspace directory where the agent will execute.
|
||||
string workspace_path = 1;
|
||||
}
|
||||
```
|
||||
|
||||
**Agent-to-Client Messages**
|
||||
|
||||
All real-time updates from the agent (including its thoughts, tool calls, and simple text replies) are streamed to the client as `TaskStatusUpdateEvents`.
|
||||
|
||||
Each Event contains a `Message` object, which holds the content in one of two formats:
|
||||
|
||||
- **TextPart**: Used for standard text messages. This part requires no custom schema.
|
||||
- **DataPart**: Used for complex, structured objects. Tool Calls and Thoughts are sent this way, each using their respective schemas defined below.
|
||||
|
||||
**Tool Calls**
|
||||
|
||||
The `ToolCall` schema is designed to provide a structured representation of a tool’s execution lifecycle. This protocol defines a clear state machine and provides detailed schemas for common development tasks (file edits, shell commands, MCP Tool), ensuring clients can build reliable UIs without being tied to a specific agent implementation.
|
||||
|
||||
The core principle is that the agent sends a `ToolCall` object on every update. This makes client-side logic stateless and simple.
|
||||
|
||||
**Tool Call Lifecycle**
|
||||
|
||||
1. **Creation**: The agent sends a `ToolCall` object with `status: PENDING`. If user permission is required, the `confirmation_request` field will be populated.
|
||||
2. **Confirmation**: If the client needs to confirm the message, the client will send a `ToolCallConfirmation`. If the client responds with a cancellation, execution will be skipped.
|
||||
3. **Execution**: Once approved (or if no approval is required), the agent sends an update with `status: EXECUTING`. It can stream real-time progress by updating the `live_content` field.
|
||||
4. **Completion**: The agent sends a final update with the status set to `SUCCEEDED`, `FAILED`, or `CANCELLED` and populates the appropriate result field.
|
||||
|
||||
**Schema**
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
// ToolCall is the central message represeting a tool's execution lifecycle.
|
||||
// The entire object is sent from the agent to client on every update.
|
||||
message ToolCall {
|
||||
// A unique identifier, assigned by the agent
|
||||
string tool_call_id = 1;
|
||||
|
||||
// The current state of the tool call in its lifecycle
|
||||
ToolCallStatus status = 2;
|
||||
|
||||
// Name of the tool being called (e.g. 'Edit', 'ShellTool')
|
||||
string tool_name = 3;
|
||||
|
||||
// An optional description of the tool call's purpose to show the user
|
||||
optional string description = 4;
|
||||
|
||||
// The structured input params provided by the LLM for tool invocation.
|
||||
google.protobuf.Struct input_parameters = 5;
|
||||
|
||||
// String containing the real-time output from the tool as it executes (primarily designed for shell output).
|
||||
// During streaming the entire string is replaced on each update
|
||||
optional string live_content = 6;
|
||||
|
||||
// The final result of the tool (used to replace live_content when applicable)
|
||||
oneof result {
|
||||
// The output on tool success
|
||||
ToolOutput output = 7;
|
||||
// The error details if the tool failed
|
||||
ErrorDetails error = 8;
|
||||
}
|
||||
|
||||
// If the tool requires user confirmation, this field will be populated while status is PENDING
|
||||
optional ConfirmationRequest confirmation_request = 9;
|
||||
}
|
||||
|
||||
// Possible execution status of a ToolCall
|
||||
enum ToolCallStatus {
|
||||
STATUS_UNSPECIFIED = 0;
|
||||
PENDING = 1;
|
||||
EXECUTING = 2;
|
||||
SUCCEEDED = 3;
|
||||
FAILED = 4;
|
||||
CANCELLED = 5;
|
||||
}
|
||||
|
||||
// ToolOuput represents the final, successful, output of a tool
|
||||
message ToolOutput {
|
||||
oneof result {
|
||||
string text = 1;
|
||||
// For ToolCalls which resulted in a file modification
|
||||
FileDiff diff = 2;
|
||||
// A generic fallback for any other structured JSON data
|
||||
google.protobuf.Struct structured_data = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// A structured representation of an error
|
||||
message ErrorDetails {
|
||||
// User facing error message
|
||||
string message = 1;
|
||||
// Optional agent-specific error type or category (e.g. read_content_failure, grep_execution_error, mcp_tool_error)
|
||||
optional string type = 2;
|
||||
// Optional status code
|
||||
optional int32 status_code = 3;
|
||||
}
|
||||
|
||||
// ConfirmationRequest is sent from the agent to client to request user permission for a ToolCall
|
||||
message ConfirmationRequest {
|
||||
// A list of choices for the user to select from
|
||||
repeated ConfirmationOption options = 1;
|
||||
// Specific details of the action requiring user confirmation
|
||||
oneof details {
|
||||
ExecuteDetails execute_details = 2;
|
||||
FileDiff file_edit_details = 3;
|
||||
McpDetails mcp_details = 4;
|
||||
GenericDetails generic_details = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// A single choice presented to the user during a confirmation request
|
||||
message ConfirmationOption {
|
||||
// Unique ID for the choice (e.g. proceed_once, cancel)
|
||||
string id = 1;
|
||||
// Human-readable choice (e.g. Allow Once, Reject).
|
||||
string name = 2;
|
||||
// An optional longer description for a tooltip
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
// Details for a request to execute a shell command
|
||||
message ExecuteDetails {
|
||||
// The shell command to be executed
|
||||
string command = 1;
|
||||
// An optional directory in which the command will be run
|
||||
optional string working_directory = 2;
|
||||
}
|
||||
|
||||
|
||||
message FileDiff {
|
||||
string file_name = 1;
|
||||
// The absolute path to the file to modify
|
||||
string file_path = 2;
|
||||
// The original content, if the file exists
|
||||
optional string old_content = 3;
|
||||
string new_content = 4;
|
||||
// Pre-formatted diff string for display
|
||||
optional string formatted_diff = 5;
|
||||
}
|
||||
|
||||
// Details for an MCP (Model Context Protocol) tool confirmation
|
||||
message McpDetails {
|
||||
// The name of the MCP server that provides the tool
|
||||
string server_name = 1;
|
||||
// THe name of the tool being called from the MCP Server
|
||||
string tool_name = 2;
|
||||
}
|
||||
|
||||
// Generic catch-all for ToolCall requests that don't fit other types
|
||||
message GenericDetails {
|
||||
// Description of the action requiring confirmation
|
||||
string description = 1;
|
||||
}
|
||||
```
|
||||
|
||||
**Agent Thoughts**
|
||||
|
||||
**Schema**
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
// Represents a thought with a subject and a detailed description.
|
||||
message AgentThought {
|
||||
// A concise subject line or title for the thought.
|
||||
string subject = 1;
|
||||
|
||||
// The description or elaboration of the thought itself.
|
||||
string description = 2;
|
||||
}
|
||||
```
|
||||
|
||||
**Event Metadata**
|
||||
|
||||
The `metadata` object in `TaskStatusUpdateEvent` is used by the A2A client to deserialize the `TaskStatusUpdateEvents` into their appropriate objects.
|
||||
|
||||
**Schema**
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
// A DevelopmentToolEvent event.
|
||||
message DevelopmentToolEvent {
|
||||
// Enum representing the specific type of development tool event.
|
||||
enum DevelopmentToolEventKind {
|
||||
// The default, unspecified value.
|
||||
DEVELOPMENT_TOOL_EVENT_KIND_UNSPECIFIED = 0;
|
||||
TOOL_CALL_CONFIRMATION = 1;
|
||||
TOOL_CALL_UPDATE = 2;
|
||||
TEXT_CONTENT = 3;
|
||||
STATE_CHANGE = 4;
|
||||
THOUGHT = 5;
|
||||
}
|
||||
|
||||
// The specific kind of event that occurred.
|
||||
DevelopmentToolEventKind kind = 1;
|
||||
|
||||
// The model used for this event.
|
||||
string model = 2;
|
||||
|
||||
// The tier of the user (optional).
|
||||
string user_tier = 3;
|
||||
|
||||
// An unexpected error occurred in the agent execution (optional).
|
||||
string error = 4;
|
||||
}
|
||||
```
|
||||
|
||||
**Client-to-Agent Messages**
|
||||
|
||||
When the agent sends a `TaskStatusUpdateEvent` with `status.state` set to `input-required` and its message contains a `ConfirmationRequest`, the client must respond by sending a new `message/stream` request.
|
||||
|
||||
This new request must include the `contextId` and the `taskId` from the ongoing task and contain a `ToolCallConfirmation` object. This object conveys the user's decision regarding the tool call that was awaiting approval.
|
||||
|
||||
**Schema**
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
// The client's response to a ConfirmationRequest.
|
||||
message ToolCallConfirmation {
|
||||
// A unique identifier, assigned by the agent
|
||||
string tool_call_id = 1;
|
||||
// The 'id' of the ConfirmationOption chosen by the user.
|
||||
string selected_option_id = 2;
|
||||
// Included if the user modifies the proposed change.
|
||||
// The type should correspond to the original ConfirmationRequest details.
|
||||
oneof modified_details {
|
||||
// Corresponds to a FileDiff confirmation
|
||||
ModifiedFileDetails file_details = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ModifiedFileDetails {
|
||||
// The new content after user edits.
|
||||
string new_content = 1;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Method Definitions
|
||||
|
||||
This section defines the new methods introduced by the `development-tool` extension.
|
||||
|
||||
**Method: `commands/get`**
|
||||
|
||||
This method allows the client to discover slash commands supported by Gemini CLI. The client should call this method during startup to dynamically populate its command list.
|
||||
|
||||
```proto
|
||||
// Response message containing the list of all top-level slash commands.
|
||||
message GetAllSlashCommandsResponse {
|
||||
// A list of the top-level slash commands.
|
||||
repeated SlashCommand commands = 1;
|
||||
}
|
||||
|
||||
// Represents a single slash command, which can contain subcommands.
|
||||
message SlashCommand {
|
||||
// The primary name of the command.
|
||||
string name = 1;
|
||||
// A detailed description of what the command does.
|
||||
string description = 2;
|
||||
// A list of arguments that the command accepts.
|
||||
repeated SlashCommandArgument arguments = 3;
|
||||
// A list of nested subcommands.
|
||||
repeated SlashCommand sub_commands = 4;
|
||||
}
|
||||
|
||||
// Defines the structure for a single slash command argument.
|
||||
message SlashCommandArgument {
|
||||
// The name of the argument.
|
||||
string name = 1;
|
||||
// A brief description of what the argument is for.
|
||||
string description = 2;
|
||||
// Whether the argument is required or optional.
|
||||
bool is_required = 3;
|
||||
}
|
||||
```
|
||||
|
||||
**Method: `command/execute`**
|
||||
|
||||
This method allows the client to execute a slash command. Following the initial `ExecuteSlashCommandResponse`, the agent will use the standard streaming mechanism to communicate the command's progress and output. All subsequent updates, including textual output, agent thoughts, and any required user confirmations for tool calls (like executing a shell command), will be sent as `TaskStatusUpdateEvent` messages, re-using the schemas defined above.
|
||||
|
||||
```proto
|
||||
// Request to execute a specific slash command.
|
||||
message ExecuteSlashCommandRequest {
|
||||
// The path to the command, e.g., ["memory", "add"] for /memory add
|
||||
repeated string command_path = 1;
|
||||
// The arguments for the command as a single string.
|
||||
string args = 2;
|
||||
}
|
||||
|
||||
// Enum for the initial status of a command execution request.
|
||||
enum CommandExecutionStatus {
|
||||
// Default unspecified status.
|
||||
COMMAND_EXECUTION_STATUS_UNSPECIFIED = 0;
|
||||
// The command was successfully received and its execution has started.
|
||||
STARTED = 1;
|
||||
// The command failed to start (e.g., command not found, invalid format).
|
||||
FAILED_TO_START = 2;
|
||||
// The command has been paused and is waiting for the user to confirm
|
||||
// a set of shell commands.
|
||||
AWAITING_SHELL_CONFIRMATION = 3;
|
||||
// The command has been paused and is waiting for the user to confirm
|
||||
// a specific action.
|
||||
AWAITING_ACTION_CONFIRMATION = 4;
|
||||
}
|
||||
|
||||
// The immediate, async response after requesting a command execution.
|
||||
message ExecuteSlashCommandResponse {
|
||||
// A unique taskID for this specific command execution.
|
||||
string execution_id = 1;
|
||||
// The initial status of the command execution.
|
||||
CommandExecutionStatus status = 2;
|
||||
// An optional message, particularly useful for explaining why a command
|
||||
// failed to start.
|
||||
string message = 3;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Separation of Concerns
|
||||
|
||||
We believe that all client-side context (ex., workspace state) and client-side tool execution (ex. read active buffers) should be routed through MCP.
|
||||
|
||||
This approach enforces a strict separation of concerns: the A2A `development-tool` extension standardizes communication to the agent, while MCP serves as the single, authoritative interface for client-side capabilities.
|
||||
|
||||
## Appendix
|
||||
|
||||
### A. Example Interaction Flow
|
||||
|
||||
1. **Client -> Server**: The client sends a `message/stream` request containing the initial prompt and configuration in an `AgentSettings` object.
|
||||
2. **Server -> Client**: SSE stream begins.
|
||||
- **Event 1**: The server sends a `Task` object with `status.state: 'submitted'` and the new `taskId`.
|
||||
- **Event 2**: The server sends a `TaskStatusUpdateEvent` with the metadata `kind` set to `'STATE_CHANGE'` and `status.state` set to `'working'`.
|
||||
3. **Agent Logic**: The agent processes the prompt and decides to call the `write_file` tool, which requires user confirmation.
|
||||
4. **Server -> Client**:
|
||||
- **Event 3**: The server sends a `TaskStatusUpdateEvent`. The metadata `kind` is `'TOOL_CALL_UPDATE'`, and the `DataPart` contains a `ToolCall` object with its `status` as `'PENDING'` and a populated `confirmation_request`.
|
||||
- **Event 4**: The server sends a final `TaskStatusUpdateEvent` for this exchange. The metadata `kind` is `'STATE_CHANGE'`, the `status.state` is `'input-required'`, and `final` is `true`. The stream for this request ends.
|
||||
5. **Client**: The client UI renders the confirmation prompt based on the `ToolCall` object from Event 3. The user clicks "Approve."
|
||||
6. **Client -> Server**: The client sends a new `message/stream` request. It includes the `taskId` from the ongoing task and a `DataPart` containing a `ToolCallConfirmation` object (e.g., `{"tool_call_id": "...", "selected_option_id": "proceed_once"}`).
|
||||
7. **Server -> Client**: A new SSE stream begins for the second request.
|
||||
- **Event 1**: The server sends a `TaskStatusUpdateEvent` with `kind: 'TOOL_CALL_UPDATE'`, containing the `ToolCall` object with its `status` now set to `'EXECUTING'`.
|
||||
- **Event 2**: After the tool runs, the server sends another `TaskStatusUpdateEvent` with `kind: 'TOOL_CALL_UPDATE'`, containing the `ToolCall` with its `status` as `'SUCCEEDED'`.
|
||||
8. **Agent Logic**: The agent receives the successful tool result and generates a final textual response.
|
||||
9. **Server -> Client**:
|
||||
- **Event 3**: The server sends a `TaskStatusUpdateEvent` with `kind: 'TEXT_CONTENT'` and a `TextPart` containing the agent's final answer.
|
||||
- **Event 4**: The server sends the final `TaskStatusUpdateEvent`. The `kind` is `'STATE_CHANGE'`, the `status.state` is `'completed'`, and `final` is `true`. The stream ends.
|
||||
10. **Client**: The client displays the final answer. The task is now complete but can be continued by sending another message with the same `taskId`.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.1.0",
|
||||
"version": "0.8.0-nightly.20250930.ddcbd0c2",
|
||||
"private": true,
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"start": "node dist/src/server.js",
|
||||
"start": "node dist/src/http/server.js",
|
||||
"build": "node ../../scripts/build_package.js",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"format": "prettier --write .",
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
|
||||
import type { Message, Task as SDKTask, AgentCard } from '@a2a-js/sdk';
|
||||
import type { Message, Task as SDKTask } from '@a2a-js/sdk';
|
||||
import type {
|
||||
TaskStore,
|
||||
AgentExecutor,
|
||||
@@ -15,8 +12,6 @@ import type {
|
||||
RequestContext,
|
||||
ExecutionEventBus,
|
||||
} from '@a2a-js/sdk/server';
|
||||
import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server';
|
||||
import { A2AExpressApp } from '@a2a-js/sdk/server/express'; // Import server components
|
||||
import type {
|
||||
ToolCallRequestInfo,
|
||||
ServerGeminiToolCallRequestEvent,
|
||||
@@ -24,18 +19,24 @@ import type {
|
||||
} from '@google/gemini-cli-core';
|
||||
import { GeminiEventType } from '@google/gemini-cli-core';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from './logger.js';
|
||||
import type { StateChange, AgentSettings } from './types.js';
|
||||
import { CoderAgentEvent } from './types.js';
|
||||
import { loadConfig, loadEnvironment, setTargetDir } from './config.js';
|
||||
import { loadSettings } from './settings.js';
|
||||
import { loadExtensions } from './extension.js';
|
||||
import { Task } from './task.js';
|
||||
import { GCSTaskStore, NoOpTaskStore } from './gcs.js';
|
||||
import type { PersistedStateMetadata } from './metadata_types.js';
|
||||
import { getPersistedState, setPersistedState } from './metadata_types.js';
|
||||
|
||||
const requestStorage = new AsyncLocalStorage<{ req: express.Request }>();
|
||||
import { logger } from '../utils/logger.js';
|
||||
import type {
|
||||
StateChange,
|
||||
AgentSettings,
|
||||
PersistedStateMetadata,
|
||||
} from '../types.js';
|
||||
import {
|
||||
CoderAgentEvent,
|
||||
getPersistedState,
|
||||
setPersistedState,
|
||||
} from '../types.js';
|
||||
import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
|
||||
import { loadSettings } from '../config/settings.js';
|
||||
import { loadExtensions } from '../config/extension.js';
|
||||
import { Task } from './task.js';
|
||||
import { requestStorage } from '../http/requestStorage.js';
|
||||
import { pushTaskStateFailed } from '../utils/executor_utils.js';
|
||||
|
||||
/**
|
||||
* Provides a wrapper for Task. Passes data from Task to SDKTask.
|
||||
@@ -77,48 +78,10 @@ class TaskWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
const coderAgentCard: AgentCard = {
|
||||
name: 'Gemini SDLC Agent',
|
||||
description:
|
||||
'An agent that generates code based on natural language instructions and streams file outputs.',
|
||||
url: 'http://localhost:41242/',
|
||||
provider: {
|
||||
organization: 'Google',
|
||||
url: 'https://google.com',
|
||||
},
|
||||
protocolVersion: '0.3.0',
|
||||
version: '0.0.2', // Incremented version
|
||||
capabilities: {
|
||||
streaming: true,
|
||||
pushNotifications: false,
|
||||
stateTransitionHistory: true,
|
||||
},
|
||||
securitySchemes: undefined,
|
||||
security: undefined,
|
||||
defaultInputModes: ['text'],
|
||||
defaultOutputModes: ['text'],
|
||||
skills: [
|
||||
{
|
||||
id: 'code_generation',
|
||||
name: 'Code Generation',
|
||||
description:
|
||||
'Generates code snippets or complete files based on user requests, streaming the results.',
|
||||
tags: ['code', 'development', 'programming'],
|
||||
examples: [
|
||||
'Write a python function to calculate fibonacci numbers.',
|
||||
'Create an HTML file with a basic button that alerts "Hello!" when clicked.',
|
||||
],
|
||||
inputModes: ['text'],
|
||||
outputModes: ['text'],
|
||||
},
|
||||
],
|
||||
supportsAuthenticatedExtendedCard: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* CoderAgentExecutor implements the agent's core logic for code generation.
|
||||
*/
|
||||
class CoderAgentExecutor implements AgentExecutor {
|
||||
export class CoderAgentExecutor implements AgentExecutor {
|
||||
private tasks: Map<string, TaskWrapper> = new Map();
|
||||
// Track tasks with an active execution loop.
|
||||
private executingTasks = new Set<string>();
|
||||
@@ -154,8 +117,8 @@ class CoderAgentExecutor implements AgentExecutor {
|
||||
|
||||
const agentSettings = persistedState._agentSettings;
|
||||
const config = await this.getConfig(agentSettings, sdkTask.id);
|
||||
const contextId =
|
||||
(metadata['_contextId'] as string) || (sdkTask.contextId as string);
|
||||
const contextId: string =
|
||||
(metadata['_contextId'] as string) || sdkTask.contextId;
|
||||
const runtimeTask = await Task.create(
|
||||
sdkTask.id,
|
||||
contextId,
|
||||
@@ -163,9 +126,7 @@ class CoderAgentExecutor implements AgentExecutor {
|
||||
eventBus,
|
||||
);
|
||||
runtimeTask.taskState = persistedState._taskState;
|
||||
await runtimeTask.geminiClient.initialize(
|
||||
runtimeTask.config.getContentGeneratorConfig(),
|
||||
);
|
||||
await runtimeTask.geminiClient.initialize();
|
||||
|
||||
const wrapper = new TaskWrapper(runtimeTask, agentSettings);
|
||||
this.tasks.set(sdkTask.id, wrapper);
|
||||
@@ -182,9 +143,7 @@ class CoderAgentExecutor implements AgentExecutor {
|
||||
const agentSettings = agentSettingsInput || ({} as AgentSettings);
|
||||
const config = await this.getConfig(agentSettings, taskId);
|
||||
const runtimeTask = await Task.create(taskId, contextId, config, eventBus);
|
||||
await runtimeTask.geminiClient.initialize(
|
||||
runtimeTask.config.getContentGeneratorConfig(),
|
||||
);
|
||||
await runtimeTask.geminiClient.initialize();
|
||||
|
||||
const wrapper = new TaskWrapper(runtimeTask, agentSettings);
|
||||
this.tasks.set(taskId, wrapper);
|
||||
@@ -322,10 +281,10 @@ class CoderAgentExecutor implements AgentExecutor {
|
||||
const sdkTask = requestContext.task as SDKTask | undefined;
|
||||
|
||||
const taskId = sdkTask?.id || userMessage.taskId || uuidv4();
|
||||
const contextId =
|
||||
const contextId: string =
|
||||
userMessage.contextId ||
|
||||
sdkTask?.contextId ||
|
||||
sdkTask?.metadata?.['_contextId'] ||
|
||||
(sdkTask?.metadata?.['_contextId'] as string) ||
|
||||
uuidv4();
|
||||
|
||||
logger.info(
|
||||
@@ -423,12 +382,21 @@ class CoderAgentExecutor implements AgentExecutor {
|
||||
const agentSettings = userMessage.metadata?.[
|
||||
'coderAgent'
|
||||
] as AgentSettings;
|
||||
wrapper = await this.createTask(
|
||||
taskId,
|
||||
contextId as string,
|
||||
agentSettings,
|
||||
eventBus,
|
||||
);
|
||||
try {
|
||||
wrapper = await this.createTask(
|
||||
taskId,
|
||||
contextId,
|
||||
agentSettings,
|
||||
eventBus,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[CoderAgentExecutor] Error creating task ${taskId}:`,
|
||||
error,
|
||||
);
|
||||
pushTaskStateFailed(error, eventBus, taskId, contextId);
|
||||
return;
|
||||
}
|
||||
const newTaskSDK = wrapper.toSDKTask();
|
||||
eventBus.publish({
|
||||
...newTaskSDK,
|
||||
@@ -639,147 +607,3 @@ class CoderAgentExecutor implements AgentExecutor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateCoderAgentCardUrl(port: number) {
|
||||
coderAgentCard.url = `http://localhost:${port}/`;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
try {
|
||||
const expressApp = await createApp();
|
||||
const port = process.env['CODER_AGENT_PORT'] || 0;
|
||||
|
||||
const server = expressApp.listen(port, () => {
|
||||
const address = server.address();
|
||||
let actualPort;
|
||||
if (process.env['CODER_AGENT_PORT']) {
|
||||
actualPort = process.env['CODER_AGENT_PORT'];
|
||||
} else if (address && typeof address !== 'string') {
|
||||
actualPort = address.port;
|
||||
} else {
|
||||
throw new Error('[Core Agent] Could not find port number.');
|
||||
}
|
||||
updateCoderAgentCardUrl(Number(actualPort));
|
||||
logger.info(
|
||||
`[CoreAgent] Agent Server started on http://localhost:${actualPort}`,
|
||||
);
|
||||
logger.info(
|
||||
`[CoreAgent] Agent Card: http://localhost:${actualPort}/.well-known/agent-card.json`,
|
||||
);
|
||||
logger.info('[CoreAgent] Press Ctrl+C to stop the server');
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[CoreAgent] Error during startup:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createApp() {
|
||||
try {
|
||||
// loadEnvironment() is called within getConfig now
|
||||
const bucketName = process.env['GCS_BUCKET_NAME'];
|
||||
let taskStoreForExecutor: TaskStore;
|
||||
let taskStoreForHandler: TaskStore;
|
||||
|
||||
if (bucketName) {
|
||||
logger.info(`Using GCSTaskStore with bucket: ${bucketName}`);
|
||||
const gcsTaskStore = new GCSTaskStore(bucketName);
|
||||
taskStoreForExecutor = gcsTaskStore;
|
||||
taskStoreForHandler = new NoOpTaskStore(gcsTaskStore);
|
||||
} else {
|
||||
logger.info('Using InMemoryTaskStore');
|
||||
const inMemoryTaskStore = new InMemoryTaskStore();
|
||||
taskStoreForExecutor = inMemoryTaskStore;
|
||||
taskStoreForHandler = inMemoryTaskStore;
|
||||
}
|
||||
|
||||
const agentExecutor = new CoderAgentExecutor(taskStoreForExecutor);
|
||||
|
||||
const requestHandler = new DefaultRequestHandler(
|
||||
coderAgentCard,
|
||||
taskStoreForHandler,
|
||||
agentExecutor,
|
||||
);
|
||||
|
||||
let expressApp = express();
|
||||
expressApp.use((req, res, next) => {
|
||||
requestStorage.run({ req }, next);
|
||||
});
|
||||
|
||||
const appBuilder = new A2AExpressApp(requestHandler);
|
||||
expressApp = appBuilder.setupRoutes(expressApp, '');
|
||||
expressApp.use(express.json());
|
||||
|
||||
expressApp.post('/tasks', async (req, res) => {
|
||||
try {
|
||||
const taskId = uuidv4();
|
||||
const agentSettings = req.body.agentSettings as
|
||||
| AgentSettings
|
||||
| undefined;
|
||||
const contextId = req.body.contextId || uuidv4();
|
||||
const wrapper = await agentExecutor.createTask(
|
||||
taskId,
|
||||
contextId,
|
||||
agentSettings,
|
||||
);
|
||||
await taskStoreForExecutor.save(wrapper.toSDKTask());
|
||||
res.status(201).json(wrapper.id);
|
||||
} catch (error) {
|
||||
logger.error('[CoreAgent] Error creating task:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error creating task';
|
||||
res.status(500).send({ error: errorMessage });
|
||||
}
|
||||
});
|
||||
|
||||
expressApp.get('/tasks/metadata', async (req, res) => {
|
||||
// This endpoint is only meaningful if the task store is in-memory.
|
||||
if (!(taskStoreForExecutor instanceof InMemoryTaskStore)) {
|
||||
res.status(501).send({
|
||||
error:
|
||||
'Listing all task metadata is only supported when using InMemoryTaskStore.',
|
||||
});
|
||||
}
|
||||
try {
|
||||
const wrappers = agentExecutor.getAllTasks();
|
||||
if (wrappers && wrappers.length > 0) {
|
||||
const tasksMetadata = await Promise.all(
|
||||
wrappers.map((wrapper) => wrapper.task.getMetadata()),
|
||||
);
|
||||
res.status(200).json(tasksMetadata);
|
||||
} else {
|
||||
res.status(204).send();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[CoreAgent] Error getting all task metadata:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error getting task metadata';
|
||||
res.status(500).send({ error: errorMessage });
|
||||
}
|
||||
});
|
||||
|
||||
expressApp.get('/tasks/:taskId/metadata', async (req, res) => {
|
||||
const taskId = req.params.taskId;
|
||||
let wrapper = agentExecutor.getTask(taskId);
|
||||
if (!wrapper) {
|
||||
const sdkTask = await taskStoreForExecutor.load(taskId);
|
||||
if (sdkTask) {
|
||||
wrapper = await agentExecutor.reconstruct(sdkTask);
|
||||
}
|
||||
}
|
||||
if (!wrapper) {
|
||||
res.status(404).send({ error: 'Task not found' });
|
||||
return;
|
||||
}
|
||||
res.json({ metadata: await wrapper.task.getMetadata() });
|
||||
});
|
||||
return expressApp;
|
||||
} catch (error) {
|
||||
logger.error('[CoreAgent] Error during startup:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Task } from './task.js';
|
||||
import type { Config, ToolCallRequestInfo } from '@google/gemini-cli-core';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
|
||||
describe('Task', () => {
|
||||
it('scheduleToolCalls should not modify the input requests array', async () => {
|
||||
const mockConfig = createMockConfig();
|
||||
|
||||
const mockEventBus: ExecutionEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
|
||||
// The Task constructor is private. We'll bypass it for this unit test.
|
||||
// @ts-expect-error - Calling private constructor for test purposes.
|
||||
const task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
task['setTaskStateAndPublishUpdate'] = vi.fn();
|
||||
task['getProposedContent'] = vi.fn().mockResolvedValue('new content');
|
||||
|
||||
const requests: ToolCallRequestInfo[] = [
|
||||
{
|
||||
callId: '1',
|
||||
name: 'replace',
|
||||
args: {
|
||||
file_path: 'test.txt',
|
||||
old_string: 'old',
|
||||
new_string: 'new',
|
||||
},
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-id-1',
|
||||
},
|
||||
];
|
||||
|
||||
const originalRequests = JSON.parse(JSON.stringify(requests));
|
||||
const abortController = new AbortController();
|
||||
|
||||
await task.scheduleToolCalls(requests, abortController.signal);
|
||||
|
||||
expect(requests).toEqual(originalRequests);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import {
|
||||
CoreToolScheduler,
|
||||
GeminiClient,
|
||||
type GeminiClient,
|
||||
GeminiEventType,
|
||||
ToolConfirmationOutcome,
|
||||
ApprovalMode,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
MCPServerStatus,
|
||||
isNodeError,
|
||||
parseAndFormatApiError,
|
||||
safeLiteralReplace,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
ToolConfirmationPayload,
|
||||
@@ -25,6 +26,7 @@ import type {
|
||||
ToolCallConfirmationDetails,
|
||||
Config,
|
||||
UserTierId,
|
||||
AnsiOutput,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { RequestContext } from '@a2a-js/sdk/server';
|
||||
import { type ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
@@ -37,10 +39,10 @@ import type {
|
||||
Artifact,
|
||||
} from '@a2a-js/sdk';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from './logger.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { CoderAgentEvent } from './types.js';
|
||||
import { CoderAgentEvent } from '../types.js';
|
||||
import type {
|
||||
CoderAgentMessage,
|
||||
StateChange,
|
||||
@@ -49,7 +51,7 @@ import type {
|
||||
TaskMetadata,
|
||||
Thought,
|
||||
ThoughtSummary,
|
||||
} from './types.js';
|
||||
} from '../types.js';
|
||||
import type { PartUnion, Part as genAiPart } from '@google/genai';
|
||||
|
||||
export class Task {
|
||||
@@ -82,18 +84,17 @@ export class Task {
|
||||
this.contextId = contextId;
|
||||
this.config = config;
|
||||
this.scheduler = this.createScheduler();
|
||||
this.geminiClient = new GeminiClient(this.config);
|
||||
this.geminiClient = this.config.getGeminiClient();
|
||||
this.pendingToolConfirmationDetails = new Map();
|
||||
this.taskState = 'submitted';
|
||||
this.eventBus = eventBus;
|
||||
this.completedToolCalls = [];
|
||||
this._resetToolCompletionPromise();
|
||||
this.config.setFlashFallbackHandler(
|
||||
async (currentModel: string, fallbackModel: string): Promise<boolean> => {
|
||||
config.setModel(fallbackModel); // gemini-cli-core sets to DEFAULT_GEMINI_FLASH_MODEL
|
||||
// Switch model for future use but return false to stop current retry
|
||||
return false;
|
||||
},
|
||||
this.config.setFallbackModelHandler(
|
||||
// For a2a-server, we want to automatically switch to the fallback model
|
||||
// for future requests without retrying the current one. The 'stop'
|
||||
// intent achieves this.
|
||||
async () => 'stop',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ export class Task {
|
||||
id: this.id,
|
||||
contextId: this.contextId,
|
||||
taskState: this.taskState,
|
||||
model: this.config.getContentGeneratorConfig().model,
|
||||
model: this.config.getModel(),
|
||||
mcpServers: servers,
|
||||
availableTools,
|
||||
};
|
||||
@@ -227,7 +228,7 @@ export class Task {
|
||||
} = {
|
||||
coderAgent: coderAgentMessage,
|
||||
model: this.config.getModel(),
|
||||
userTier: this.geminiClient.getUserTier(),
|
||||
userTier: this.config.getUserTier(),
|
||||
};
|
||||
|
||||
if (metadataError) {
|
||||
@@ -285,20 +286,29 @@ export class Task {
|
||||
|
||||
private _schedulerOutputUpdate(
|
||||
toolCallId: string,
|
||||
outputChunk: string,
|
||||
outputChunk: string | AnsiOutput,
|
||||
): void {
|
||||
let outputAsText: string;
|
||||
if (typeof outputChunk === 'string') {
|
||||
outputAsText = outputChunk;
|
||||
} else {
|
||||
outputAsText = outputChunk
|
||||
.map((line) => line.map((token) => token.text).join(''))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'[Task] Scheduler output update for tool call ' +
|
||||
toolCallId +
|
||||
': ' +
|
||||
outputChunk,
|
||||
outputAsText,
|
||||
);
|
||||
const artifact: Artifact = {
|
||||
artifactId: `tool-${toolCallId}-output`,
|
||||
parts: [
|
||||
{
|
||||
kind: 'text',
|
||||
text: outputChunk,
|
||||
text: outputAsText,
|
||||
} as Part,
|
||||
],
|
||||
};
|
||||
@@ -509,7 +519,9 @@ export class Task {
|
||||
if (oldString === '' && !isNewFile) {
|
||||
return currentContent;
|
||||
}
|
||||
return currentContent.replaceAll(oldString, newString);
|
||||
|
||||
// Use intelligent replacement that handles $ sequences safely
|
||||
return safeLiteralReplace(currentContent, oldString, newString);
|
||||
}
|
||||
|
||||
async scheduleToolCalls(
|
||||
@@ -520,30 +532,36 @@ export class Task {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const request of requests) {
|
||||
if (
|
||||
!request.args['newContent'] &&
|
||||
request.name === 'replace' &&
|
||||
request.args &&
|
||||
request.args['file_path'] &&
|
||||
request.args['old_string'] &&
|
||||
request.args['new_string']
|
||||
) {
|
||||
request.args['newContent'] = await this.getProposedContent(
|
||||
request.args['file_path'] as string,
|
||||
request.args['old_string'] as string,
|
||||
request.args['new_string'] as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
const updatedRequests = await Promise.all(
|
||||
requests.map(async (request) => {
|
||||
if (
|
||||
request.name === 'replace' &&
|
||||
request.args &&
|
||||
!request.args['newContent'] &&
|
||||
request.args['file_path'] &&
|
||||
request.args['old_string'] &&
|
||||
request.args['new_string']
|
||||
) {
|
||||
const newContent = await this.getProposedContent(
|
||||
request.args['file_path'] as string,
|
||||
request.args['old_string'] as string,
|
||||
request.args['new_string'] as string,
|
||||
);
|
||||
return { ...request, args: { ...request.args, newContent } };
|
||||
}
|
||||
return request;
|
||||
}),
|
||||
);
|
||||
|
||||
logger.info(`[Task] Scheduling batch of ${requests.length} tool calls.`);
|
||||
logger.info(
|
||||
`[Task] Scheduling batch of ${updatedRequests.length} tool calls.`,
|
||||
);
|
||||
const stateChange: StateChange = {
|
||||
kind: CoderAgentEvent.StateChangeEvent,
|
||||
};
|
||||
this.setTaskStateAndPublishUpdate('working', stateChange);
|
||||
|
||||
await this.scheduler.schedule(requests, abortSignal);
|
||||
await this.scheduler.schedule(updatedRequests, abortSignal);
|
||||
}
|
||||
|
||||
async acceptAgentMessage(event: ServerGeminiStreamEvent): Promise<void> {
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { logger } from './logger.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import type { Settings } from './settings.js';
|
||||
import type { Extension } from './extension.js';
|
||||
import { type AgentSettings, CoderAgentEvent } from './types.js';
|
||||
import { type AgentSettings, CoderAgentEvent } from '../types.js';
|
||||
|
||||
export async function loadConfig(
|
||||
settings: Settings,
|
||||
@@ -69,6 +69,7 @@ export async function loadConfig(
|
||||
settings.fileFiltering?.enableRecursiveFileSearch,
|
||||
},
|
||||
ideMode: false,
|
||||
folderTrust: settings.folderTrust === true,
|
||||
};
|
||||
|
||||
const fileService = new FileDiscoveryService(workspaceDir);
|
||||
@@ -79,10 +80,10 @@ export async function loadConfig(
|
||||
false,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
settings.folderTrust === true,
|
||||
);
|
||||
configParams.userMemory = memoryContent;
|
||||
configParams.geminiMdFileCount = fileCount;
|
||||
|
||||
const config = new Config({
|
||||
...configParams,
|
||||
});
|
||||
@@ -108,9 +109,10 @@ export async function loadConfig(
|
||||
logger.info('[Config] Using Gemini API Key');
|
||||
await config.refreshAuth(AuthType.USE_GEMINI);
|
||||
} else {
|
||||
logger.error(
|
||||
`[Config] Unable to set GeneratorConfig. Please provide a GEMINI_API_KEY or set USE_CCPA.`,
|
||||
);
|
||||
const errorMessage =
|
||||
'[Config] Unable to set GeneratorConfig. Please provide a GEMINI_API_KEY or set USE_CCPA.';
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return config;
|
||||
@@ -10,7 +10,7 @@ import type { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { logger } from './logger.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
|
||||
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user