Compare commits

...

317 Commits

Author SHA1 Message Date
gemini-cli-robot a94599e478 chore(release): v0.19.0-nightly.20251121.5982abeff 2025-11-21 02:39:05 +00:00
Sandy Tao 5982abeffb fix(ui): Correct mouse click cursor positioning for wide characters (#13537) 2025-11-20 23:56:31 +00:00
Vish (Ishaya) Abrams 78b10dccf1 Skip pre-commit hooks for shadow repo (#13331) (#13488)
Co-authored-by: Vishvananda Abrams <vabrams@google.com>
2025-11-20 22:50:36 +00:00
gemini-cli-robot 85bc25a765 chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 (#13540) 2025-11-20 22:43:11 +00:00
Jacob Richman fec0eba07e move stdio (#13528) 2025-11-20 22:16:46 +00:00
Tommaso Sciortino f92e79eba0 update docs for http proxy (#13538) 2025-11-20 22:13:59 +00:00
Adib234 3e50be1658 Update persistence state to track counts of messages instead of times banner has been displayed (#13428) 2025-11-20 21:57:06 +00:00
cornmander 9937fb2220 Use lenient MCP output schema validator (#13521) 2025-11-20 21:51:25 +00:00
joshualitt 8e531dc029 feat(core): Incorporate Gemini 3 into model config hierarchy. (#13447) 2025-11-20 21:07:14 +00:00
Ben Brandt 83d0bdc32e feat(zed-integration): Use default model routing for Zed integration (#13398)
Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
2025-11-20 20:08:37 +00:00
Tommaso Sciortino d15970e12c remove duplicated mouse code (#13525) 2025-11-20 11:46:53 -08:00
Tommaso Sciortino c7b5dcd28f Change default compress threshold to 0.5 for api key users (#13517) 2025-11-20 11:43:35 -08:00
joshualitt 8c07ad2ab9 feat(core): Use thinking level for Gemini 3 (#13445) 2025-11-20 19:19:52 +00:00
Adib234 ade9dfeebb Enable switching preview features on/off without restart (#13515) 2025-11-20 19:00:16 +00:00
Jacob Richman d1e35f8660 Protect stdout and stderr so JavaScript code can't accidentally write to stdout corrupting ink rendering (#13247)
Bypassing rules as link checker failure is spurious.
2025-11-20 10:44:02 -08:00
Adib234 e20d282088 Update banner design (#13420) 2025-11-20 16:52:15 +00:00
Gaurav Sehgal 4adfdad47f Copy commands as part of setup-github (#13464) 2025-11-20 16:05:15 +00:00
Ben Brandt 6c126b9e58 fix(zed-integration): Ensure that the zed integration is classified as interactive (#13394)
Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
2025-11-20 15:28:37 +00:00
lifefloating 1d2e27a698 Fix: Update system instruction when GEMINI.md memory is loaded or refreshed (#12136) 2025-11-20 13:22:06 +00:00
Adam Weidman 049a299b92 Revert "feat(core): Fix bug with incorrect model overriding." (#13483) 2025-11-20 06:39:21 +00:00
Sandy Tao b1258dd52c fix(cli): prevent race condition when restoring prompt after context overflow (#13473) 2025-11-20 05:54:16 +00:00
Greg Shikhman 98cdaa01b8 Revert "build bun"
This reverts commit b0938f9102.
2025-11-20 06:02:54 +00:00
Greg Shikhman 3476a97acc build bun 2025-11-20 06:02:54 +00:00
Tommaso Sciortino 9ebf321717 Use synchronous writes when detecting keyboard modes (#13478) 2025-11-20 05:50:05 +00:00
joshualitt ff725dea41 feat(core): Fix bug with incorrect model overriding. (#13477) 2025-11-20 05:48:08 +00:00
gemini-cli-robot c3f1b29c1e chore/release: bump version to 0.18.0-nightly.20251120.2231497b1 (#13476) 2025-11-20 05:05:32 +00:00
joshualitt 257cd07a3a feat(core): Wire up chat code path for model configs. (#12850) 2025-11-20 04:41:16 +00:00
Preston Holmes 43d6dc3668 Add User email detail to about box (#13459) 2025-11-20 03:46:21 +00:00
Gal Zahavi 2231497b1f feat: add click-to-focus support for interactive shell (#13341) 2025-11-19 23:49:39 +00:00
Tommaso Sciortino d0a845b6e6 remove unneeded log (#13456) 2025-11-19 23:42:21 +00:00
kevin-ramdass e4c4bb265e feat: update thinking mode support to exclude gemini-2.0 models and simplify logic. (#13454) 2025-11-19 23:35:35 +00:00
Smetalo 0f845407f1 Fix typo in write_todos methodology instructions (#13411) 2025-11-19 23:29:12 +00:00
Tommaso Sciortino b2a2ea3633 Fix Copyright line in LICENSE (#13449) 2025-11-19 22:53:34 +00:00
niladri das f8a862738d [cli-ui] Keep header ASCII art colored on non-gradient terminals (#13373) (#13374)
Co-authored-by: Niladri Das <bniladridas@users.noreply.github.com>
2025-11-19 22:04:58 +00:00
joshualitt 25f8452127 feat(core, cli): Bump genai version to 1.30.0 (#13435) 2025-11-19 20:52:27 +00:00
Tommaso Sciortino 84573992b4 Restore keyboard mode when exiting the editor (#13350) 2025-11-19 19:37:30 +00:00
Holt Skinner 3f8d636501 docs: Add Code Wiki link to README (#13289) 2025-11-19 18:15:41 +00:00
Ben Brandt 300205b07c fix(zed-integration): Correctly handle cancellation errors (#13399) 2025-11-19 17:25:52 +00:00
bl-ue e1c711f5ba feat(sessions): record interactive-only errors and warnings to chat recording JSON files (#13300) 2025-11-19 16:32:13 +00:00
Jainam M 282654e7b8 Capturing github action workflow name if present and send it to clearcut (#13132) 2025-11-19 16:30:03 +00:00
bl-ue 0d89ac7406 Followup from #10719 (#13243) 2025-11-19 16:22:17 +00:00
davidmcwherter 5c47592159 Refactor createTransport to duplicate less code (#13010)
Co-authored-by: David McWherter <davidmcw@gmail.com>
2025-11-19 15:23:01 +00:00
Jack Wotherspoon 61f0f3c243 fix: allow MCP prompts with spaces in name (#12910)
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
2025-11-19 14:44:30 +00:00
avilladsen 1e8ae5b9d7 fix(cli): fix crash on startup in NO_COLOR mode (#13343) due to ungua… (#13352) 2025-11-19 04:18:18 +00:00
Jacob Richman e8d0e0d342 bug(ui) showLineNumbers had the wrong default value. (#13356) 2025-11-19 03:33:40 +00:00
Sandy Tao b644f037a3 fix(ui): Clear input prompt on Escape key press (#13335) 2025-11-19 03:11:36 +00:00
Gal Zahavi c5498bbb07 Improve pty resize error handling for Windows (#13353) 2025-11-19 02:08:01 +00:00
Tommaso Sciortino 90c764ce13 Support 3-parameter modifyOtherKeys sequences (#13342) 2025-11-19 00:40:40 +00:00
joshualitt 10003a6490 fix(core): Ensure read_many_files tool is available to zed. (#13338) 2025-11-18 22:58:12 +00:00
Tommaso Sciortino b916d79fe2 Improve keyboard code parsing (#13307) 2025-11-18 19:49:08 +00:00
Gray Mackall 36b0a86c69 Fix links in Gemini 3 Pro documentation (#13312) 2025-11-18 19:39:51 +00:00
Jenna Inouye 7cc5234b9c Docs: Access clarification. (#13304) 2025-11-18 19:31:51 +00:00
Shreya Keshive 379f09d92f chore(release): v0.18.0-nightly.20251118.86828bb56 (#13309) 2025-11-18 19:04:29 +00:00
Aswin Ashok fd9d3e19a9 Remove obsolete reference to "help wanted" label in CONTRIBUTING.md (#13291) 2025-11-18 18:17:07 +00:00
Shreya Keshive 86828bb561 feat: launch Gemini 3 in Gemini CLI 🚀🚀🚀 (in main) (#13287)
Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com>
Co-authored-by: Sehoon Shon <sshon@google.com>
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
Co-authored-by: Sandy Tao <sandytao520@icloud.com>
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
Co-authored-by: Aishanee Shah <aishaneeshah@gmail.com>
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
Co-authored-by: Jacob Richman <jacob314@gmail.com>
Co-authored-by: joshualitt <joshualitt@google.com>
Co-authored-by: Jenna Inouye <jinouye@google.com>
2025-11-18 09:01:16 -08:00
Jenna Inouye 78075c8a37 Docs: Add changelog for v.0.15.0 (#13276) 2025-11-18 16:09:55 +00:00
JAYADITYA 7d33baabe4 feat :uninstall multiple extensions (#13016)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-18 15:20:25 +00:00
JAYADITYA ecf8fba10e feat: Show tip on first request and refactor phrases (#12952)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2025-11-18 07:02:31 +00:00
Tommaso Sciortino 2f8b68dffd update glob version (#13242) 2025-11-18 00:31:17 +00:00
Tommaso Sciortino 8877c85278 Right click to paste in Alternate Buffer mode (#13234) 2025-11-17 23:48:33 +00:00
Riddhi Dutta 1d1bdc57ce Refactored 4 files of tools package (#13235)
Co-authored-by: riddhi <duttariddhi@google.com>
2025-11-17 19:31:29 +00:00
Tommaso Sciortino ba88707b1e Fix test to not leave terminal in mouse mode (#13232) 2025-11-17 17:47:08 +00:00
cornmander 8c78fe4f10 rework MCP tool discovery and invocation (#13160)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-17 17:03:48 +00:00
Riddhi Dutta 394a7ea019 Refactored 3 files of tools package (#13231)
Co-authored-by: riddhi <duttariddhi@google.com>
2025-11-17 16:54:09 +00:00
Jacob Richman 78a28bfc01 Fix: Animated scrollbar renders black in NO_COLOR mode (#13188) 2025-11-17 04:45:07 +00:00
gemini-cli-robot cf8de02c68 chore/release: bump version to 0.17.0-nightly.20251116.e650a4ee5 (#13154) 2025-11-16 02:32:39 +00:00
Riddhi Dutta e650a4ee59 Refactored core package ut (#13139)
Co-authored-by: riddhi <duttariddhi@google.com>
2025-11-15 17:23:21 +00:00
Samuel Soto c6b6dcbe95 Docs: Clarify Project-Scoped Behavior of Chat Sub-commands (#10458)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-15 15:50:30 +00:00
shrutip90 9786c4dcff Check folder trust before allowing add directory (#12652) 2025-11-15 03:06:30 +00:00
Tommaso Sciortino d03496b710 Increase paste timeout + add warning. (#13099) 2025-11-15 00:02:28 +00:00
Jacob Richman ab6b2293e1 Only warn about mouse movement when the mouse is down. (#13101) 2025-11-14 22:55:05 +00:00
shrutip90 472e775a13 feat: Update permissions command to support modifying trust for other… (#11642) 2025-11-14 22:41:53 +00:00
Adib234 ce56b4ee1b Change flag name to flag id for existing flags (#13073) 2025-11-14 21:34:54 +00:00
Jacob Richman ba15eeb55f bug(ui) make it clear when users need to enter selection mode and fix clear issue. (#13083) 2025-11-14 20:02:15 +00:00
shrutip90 d683e1c0db fix(cli): Exit CLI when trust save unsuccessful during launch (#11968) 2025-11-14 19:56:39 +00:00
Tommaso Sciortino 6d83d3440c Change default compress threshold to 0.7 for api key users (#13079) 2025-11-14 17:13:59 +00:00
Caroline Rose 9d74b7c0e8 feat(auth): Add option for metadata server application default credentials without project override (#12948) 2025-11-14 16:39:11 +00:00
Stone Sun 016b5b42e2 docs: remove references to deprecated --checkpointing flag (#12477)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2025-11-14 16:19:12 +00:00
YongSung Na a591505bf9 docs: remove references to deprecated flags (--checkpointing, --sandbox-image) (#12578)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2025-11-14 15:28:14 +00:00
Megha Bansal 638dd2f6c0 Improve test code coverage for cli/command/extensions package (#12994) 2025-11-14 05:28:01 +00:00
Tommaso Sciortino 555e25e633 slightly adjust model message formatting (#13043) 2025-11-14 05:13:39 +00:00
gemini-cli-robot 3174573b64 chore/release: bump version to 0.17.0-nightly.20251114.0fcbff506 (#13040) 2025-11-14 04:07:27 +00:00
Tommaso Sciortino 2c5e09e1c3 Update comment and undo unnecessary logging (#13025) 2025-11-14 03:11:13 +00:00
Tommaso Sciortino ab11b2c27f Show model in history (#13034) 2025-11-14 03:11:06 +00:00
gemini-cli-robot 0fcbff506e chore(release): bump version to 0.17.0-nightly.20251113.48e3932f6 (#13027) 2025-11-14 00:19:33 +00:00
joshualitt 48e3932f65 feat(core, cli): Add auth type to history checkpoint. (#13023) 2025-11-13 23:13:39 +00:00
Jacob Richman d304216300 Polish sticky headers (#13024) 2025-11-13 22:33:48 +00:00
Jacob Richman 2b8adf8cf3 jacob314/drag scrollbar (#12998) 2025-11-13 22:13:18 +00:00
Jacob Richman fb99b95372 Switch back to truncating headers. Unfortunately we have too many headers that are 10+ lines for no good reason so we need to add back this truncation until we fix them. (#13018) 2025-11-13 19:38:19 +00:00
Jacob Richman 60fe5acd60 feat(ui) support animated page up/down, fn-up/down and end+home (#13012) 2025-11-13 19:16:23 +00:00
Jacob Richman eb9ff72b5a Support incremental update experiment flag. (#12926) 2025-11-13 17:45:03 +00:00
Tommaso Sciortino fb03242950 Improve MALFORMED_FUNCTION_CALL handling (#12965) 2025-11-13 16:07:49 +00:00
Adam Weidman 54c1e13853 chore: update package lock (#12974) 2025-11-13 14:50:14 +00:00
N. Taylor Mullen 102905bbc7 feat: normalize verticality (#12991) 2025-11-13 08:20:14 +00:00
N. Taylor Mullen fe1bfc64f7 feat: disengage surface adhesion protocols (#12989) 2025-11-13 07:36:18 +00:00
Jacob Richman 0f9ec2735c feat(ui) Make useAlternateBuffer the default (#12976) 2025-11-13 05:17:46 +00:00
Abhi 13d8d9477c fix(editor): ensure preferred editor setting updates immediately (#12981) 2025-11-13 04:02:53 +00:00
Tommaso Sciortino 43916b98aa Don't clear buffers on cleanup. (#12979) 2025-11-13 03:20:21 +00:00
Abhi 5d27a62bec refactor: remove read-many-files tool from agent (#12796) 2025-11-13 02:56:37 +00:00
gemini-cli-robot a05e0ea3a4 chore/release: bump version to 0.16.0-nightly.20251113.ad1f0d99 (#12977) 2025-11-13 02:03:52 +00:00
anthony bushong 2987b473dd feat(core): set default chat base model configs (#12951) 2025-11-13 01:01:31 +00:00
Jacob Richman ee7065f665 Sticky headers where the top rounded border is sticky. (#12971) 2025-11-13 01:01:16 +00:00
anthony bushong d26b828ab3 feat(core): update default model config in client.ts (#12960) 2025-11-13 00:52:04 +00:00
Sehoon Shon a810ca80bc Allow users to reset to auto when it's in fallback mode (#12623) 2025-11-13 00:30:43 +00:00
Allen Hutchison ad1f0d995d refactor: move toml-loader.test.ts to use real filesystem (#12969) 2025-11-12 23:17:38 +00:00
Allen Hutchison 1ed163a666 feat(safety): Introduce safety checker framework (#12504) 2025-11-12 21:18:34 +00:00
cornmander aa9922bc98 feat: autogenerate keyboard shortcut docs (#12944) 2025-11-12 21:07:14 +00:00
Tommaso Sciortino 0075b4f118 Always show the tool internal name in /tools (#12964) 2025-11-12 20:45:18 +00:00
Jacob Richman 4d85ce40be Turns out the node console.clear() clears the buffer. (#12959) 2025-11-12 20:15:25 +00:00
anj-s 7ec78452ec Enable write_todo tool and fix output function schema (#12905) 2025-11-12 18:18:15 +00:00
Abhi 540f60696a fix(docs): Release version for read many files removal (#12949) 2025-11-12 18:05:16 +00:00
Shardul Natu 1ffb9c4188 fix(FileCommandLoader): Remove error logs if the operation was aborted (#12927)
Co-authored-by: Shnatu <snatu@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-12 16:33:28 +00:00
Shreya Keshive 570ccc7da0 feat: Update client metadata for consistency (#12947) 2025-11-12 16:11:48 +00:00
Edilmo Palencia 1c8fe92d0f feat(hooks): Hook Result Aggregation (#9095) 2025-11-12 07:14:09 +00:00
gemini-cli-robot 396b427cc9 chore/release: bump version to 0.16.0-nightly.20251112.c961f274 (#12923) 2025-11-12 06:13:46 +00:00
gemini-cli-robot c961f27401 chore(release): bump version to 0.16.0-nightly.20251112.2abc288c (#12917)
Co-authored-by: Sandy Tao <sandytao520@icloud.com>
2025-11-12 05:14:20 +00:00
Abhi 1c87e7cd25 feat: enhance RipGrep tool with advanced search options and improved defaults (#12677) 2025-11-12 05:11:19 +00:00
Sandy Tao 408b885689 feat(core): enhance loop detection with 2-stage check (#12902) 2025-11-12 04:49:00 +00:00
N. Taylor Mullen cab9b1f370 Fix extensions disable/enable commands not awaiting handler (#12915) 2025-11-12 04:31:46 +00:00
Tommaso Sciortino d3cf28eb4b Use PascalCase for all tool display names (#12918) 2025-11-12 04:28:13 +00:00
Megha Bansal 11a0a9b911 Increase code coverage for core packages (#12872) 2025-11-12 04:06:43 +00:00
Tommaso Sciortino e8038c727f fix test to use faketimer (#12913) 2025-11-12 03:03:10 +00:00
Jacob Richman 2abc288c52 Make useFullWidth the default. (#12911) 2025-11-12 00:14:08 +00:00
Tommaso Sciortino 2e2b066713 Move temp dir from system prompt to first user msg (#12895) 2025-11-11 21:22:22 +00:00
Abhi c1076512d1 Deprecate read_many_files tool (#12861) 2025-11-11 20:35:20 +00:00
Abhi 3154c06dc3 fix(ci): pre-download ripgrep in global setup to prevent race conditions (#12886) 2025-11-11 18:50:17 +00:00
kevinjwang1 b248ec6dfb Add implementation for setting to disable Github extensions (#12838) 2025-11-11 18:37:01 +00:00
Shardul Natu cc2c48d59e fix(extension-uninstallation): Fix uninstalling extensions named differently than their directory name (#12852)
Co-authored-by: Shnatu <snatu@google.com>
2025-11-11 18:19:06 +00:00
Tommaso Sciortino ac733d40b3 Add expected_replacements to smart-edit tool (#12885) 2025-11-11 18:17:45 +00:00
joshualitt a4415f15d3 feat(core): Migrate generateContent to model configs. (#12834) 2025-11-11 16:10:50 +00:00
Jacob Richman cbbf565121 Support ink scrolling final pr (#12567) 2025-11-11 15:50:11 +00:00
N. Taylor Mullen 7bb13d1c41 telemetry: track interactive session state (#12856)
Co-authored-by: owenofbrien <86964623+owenofbrien@users.noreply.github.com>
2025-11-11 10:03:32 +00:00
gemini-cli-robot e79f62694a chore/release: bump version to 0.15.0-nightly.20251111.51f952e7 (#12860) 2025-11-11 07:04:14 +00:00
cornmander 4fbeac8b32 Add experiment logging and add caching experiment (#12862) 2025-11-11 04:15:38 +00:00
Megha Bansal 22b0550520 Issue#9751 - fix the gemini crash on startup in tmux environments (#11637) 2025-11-11 03:57:43 +00:00
Mark Thompson 5ba6bc7139 fix(prompt): Add Angular support to base prompt (#10252) 2025-11-11 03:42:22 +00:00
bl-ue 6893d27441 feat(sessions): add resuming to geminiChat and add CLI flags for session management (#10719)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-11 01:31:00 +00:00
Abhi 51f952e700 fix(core): use ripgrep --json output for robust cross-platform parsing (#12853) 2025-11-11 00:01:01 +00:00
davidmcwherter 4af4f8644d Fixes #12841: Plumb headers through google_credentials transport crea… (#12849)
Co-authored-by: David McWherter <davidmcw@gmail.com>
2025-11-10 22:06:31 +00:00
cornmander 37ca643a64 Fix external editor diff drift (#12846) 2025-11-10 21:19:32 +00:00
Jacob Richman 8f4b1b582d Switch to alternate buffer mode before rendering Ink. (#12833) 2025-11-10 20:30:38 +00:00
Tommaso Sciortino 3c9052a751 Stop printing garbage characters for F1,F2.. keys (#12835) 2025-11-10 18:56:05 +00:00
cornmander 2136598e87 Harden modifiable tool temp workspace (#12837)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-10 18:48:44 +00:00
owenofbrien 3f90001f83 Added active experiment ids to Clearcut log events (#12745) 2025-11-10 17:21:49 +00:00
Edilmo Palencia 4ef4bd6f09 feat(hooks): Hook Execution Engine (#9092) 2025-11-10 16:08:21 +00:00
Adib234 331dbd5638 Preserve tabs on paste (#12735) 2025-11-10 15:37:43 +00:00
Hriday 4ab94dec5e test: fix flaky file system integration test (#12724)
Co-authored-by: Hriday Taneja <hridayt@google.com>
2025-11-10 06:20:30 +00:00
Tommaso Sciortino c0b766ad74 Simplify switch case (#12798) 2025-11-09 22:36:13 +00:00
Tommaso Sciortino fd59d9dd92 Fix shift+return in vscode. (#12799) 2025-11-09 22:08:29 +00:00
Tommaso Sciortino 9e4ae214a8 Revamp KeypressContext (#12746) 2025-11-09 16:45:04 +00:00
Jacob Richman f649948713 Branch batch scroll (#12680) 2025-11-09 00:09:22 +00:00
christine betts 43b8731241 Fix extensions logging race condition and slash command logging (#12732) 2025-11-08 15:29:36 +00:00
Jacob MacDonald 9116cf2bab [cleanup] rename info message property 'icon' to 'prefix' (#12743) 2025-11-08 00:07:33 +00:00
Jacob MacDonald bafbcbbe8b [feat]: Add /extensions restart command (#12739) 2025-11-07 23:17:23 +00:00
joshualitt fdb6088603 feat(core): Migrate generateJson to resolved model configs. (#12626) 2025-11-07 22:18:45 +00:00
Sandy Tao f3a8b73717 fix(ci): ensure correct version calculation and git ls-remote filtering in release workflow (#12683) 2025-11-07 21:06:55 +00:00
Jacob MacDonald c883403147 [feat] Extension Reloading - respect updates to exclude tools (#12728) 2025-11-07 20:18:35 +00:00
joshualitt 2077521f84 Trivial yaml fixes for linter. (#12725) 2025-11-07 18:37:36 +00:00
Shreya Keshive 6d90b7ddb3 feat(issue-templates): Refine issue template labels and types
Updated  and  to use a  field instead of  and  labels.
Modified  to use  instead of  and removed .

This change streamlines issue categorization and aligns with updated labeling conventions.
2025-11-07 12:46:18 -05:00
Jacob Richman 6cf1c98524 Update ink version (#12720) 2025-11-07 17:32:46 +00:00
Allen Hutchison 5ff7cdc9e8 test(policy): add extreme priority value tests (follow-up to #12617) (#12679) 2025-11-07 17:28:05 +00:00
Jacob MacDonald 47603ef8e1 Reload gemini memory on extension load/unload + memory refresh refactor (#12651) 2025-11-07 17:07:25 +00:00
Lê Nam Khánh ef4030331a docs: fix typos in some files (#12706)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-07 15:51:56 +00:00
Haroon Chaudhry a0a6828265 fix: Downloading release assets from private GitHub repository #11928 (#12682) 2025-11-07 15:50:44 +00:00
cocosheng-g 69339f08a6 Adds listCommands endpoint to a2a server (#12604)
Co-authored-by: Juanda <jdgarrido@google.com>
Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
2025-11-07 15:10:29 +00:00
gemini-cli-robot cd27cae848 chore(release): bump version to 0.15.0-nightly.20251107.b8eeb553 (#12687)
Co-authored-by: Sandy Tao <sandytao520@icloud.com>
2025-11-07 04:29:38 +00:00
Tommaso Sciortino 475e92da5d Fix test in windows (#12681) 2025-11-07 04:01:30 +00:00
Shreya Keshive 054497c7a6 fix(core): Handle null command in VSCode IDE detection (#12689) 2025-11-07 03:01:53 +00:00
Allen Hutchison c81a02f8d2 fix: integrate DiscoveredTool with Policy Engine (#12646) 2025-11-06 23:51:16 +00:00
Richie Foreman 445a5eac33 fix(patch workflow): Ensure that the environment is listed on patch comments (#12538) 2025-11-06 23:30:20 +00:00
Tommaso Sciortino f05d937f39 Use consistent param names (#12517) 2025-11-06 23:03:52 +00:00
HugoMurillo 5f1208ad81 chore: disable flaky test (#12670) 2025-11-06 22:43:55 +00:00
Adam Weidman 1e42fdf6c2 fix(cli): handle flash model errors gracefully (#12667) 2025-11-06 22:13:38 +00:00
Jenna Inouye 98055d0989 Docs: Add /model documentation (#12654) 2025-11-06 21:12:52 +00:00
christine betts c13ec85d7d Update keychain storage name to be more user-friendly (#12644) 2025-11-06 20:55:20 +00:00
Sandy Tao 36feb73bfd Revert "Enable WriteTodos tool by default (#12500)" (#12658) 2025-11-06 20:21:53 +00:00
HugoMurillo 77614eff5b fix(#11707): should replace multiple instances of a string test (#12647) 2025-11-06 19:12:06 +00:00
Sehoon Shon 31b34b11ab Let users know when falling back to flash, and update the error messa… (#12640) 2025-11-06 18:43:21 +00:00
Jainam M c585470a71 refactor(cli): consolidate repetitive tests in InputPrompt using it.each (#12524) 2025-11-06 17:41:50 +00:00
Gal Zahavi 9ba1cd0336 feat(shell): include cwd in shell command description (#12558) 2025-11-06 16:51:07 +00:00
Allen Hutchison 5f6453a1e0 feat(policy): Add comprehensive priority range validation tests (#12617) 2025-11-06 07:19:05 +00:00
joshualitt 956ab94452 feat(core): Add ModelConfigService. (#12556) 2025-11-06 01:18:42 +00:00
Allen Hutchison 0f5dd2229c chore: remove unused CLI policy TOML files (#12620) 2025-11-06 01:17:44 +00:00
Jacob Richman 224a33db2e Improve tracking of animated components. (#12618) 2025-11-06 00:20:04 +00:00
Jenna Inouye fb0768f007 Docs: Added newest changelog: v0.12.0 (#12611) 2025-11-05 23:44:24 +00:00
Tommaso Sciortino 9787108532 List tools in a consistent order. (#12615) 2025-11-05 23:38:44 +00:00
Andrew Riggs 44b8c62db9 fix(core) Path reader method readPathFromWorkspace does not respect git/gemini ignore config. (#10073)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2025-11-05 21:40:54 +00:00
Silvio Junior 1d2f90c7e7 Add compression mechanism to subagent (#12506) 2025-11-05 21:15:28 +00:00
Adam Weidman c951f9fdcd fix: add line breaks in quota/capacity msgs (#12603) 2025-11-05 21:12:06 +00:00
Jacob MacDonald fa93b56243 [Extension Reloading]: Update custom commands, add enable/disable command (#12547) 2025-11-05 19:36:07 +00:00
Tommaso Sciortino ca6cfaaf4e Update auto compression message. (#12605) 2025-11-05 19:08:57 +00:00
Shreya Keshive 400da30a8d fix(triage-workflow): Pass environment variables directly into prompt (#12602) 2025-11-05 18:33:33 +00:00
Allen Hutchison f5bd474e51 fix(core): prevent server name spoofing in policy engine (#12511) 2025-11-05 18:10:23 +00:00
Bryan Morgan 16113647de Fix/windows pty crash (#12587)
Co-authored-by: LayorX <yor31117@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-05 16:53:03 +00:00
Gaurav f51d74586c refactor: parse string for retryInfo (#12586) 2025-11-05 16:42:58 +00:00
Shreya Keshive c743631148 chore(release): bump version to 0.14.0-nightly.20251104.da3da198 (#12564) 2025-11-05 16:09:33 +00:00
shishu314 b445db3d46 fix(infra) - Make list dir less flaky (#12554)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-11-05 16:00:06 +00:00
Adam Weidman 21dd9bbf7d fix: allow user to set pro model even in fallback (#12566) 2025-11-05 14:38:45 +00:00
Jacob Richman 3937461272 Scrollable support (#12544) 2025-11-05 00:21:00 +00:00
Shreya Keshive da3da19844 feat(core): Integrate remote experiments configuration (#12539) 2025-11-04 23:09:53 +00:00
Silvio Junior 53c7646ed7 enable codebase investigator by default for preview (#12555) 2025-11-04 22:30:01 +00:00
Sehoon Shon b6524e410a migrate console.error to coreEvents/debugger for sandbox, logger, chatRecordingService (#12253) 2025-11-04 21:02:31 +00:00
shishu314 75c2769b32 Ss/fix ext (#12540)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-11-04 20:01:30 +00:00
Tommaso Sciortino f796650122 Fix shift+tab keybinding when not in kitty mode (#12552) 2025-11-04 19:45:22 +00:00
christine betts 460c3debf5 Fix flicker in screen reader nudge (#12541) 2025-11-04 16:57:15 +00:00
cornmander b8b6620365 Tighten bash shell option handling (#12532) 2025-11-04 16:11:29 +00:00
shishu314 96d7eb2966 fix(infra) - Use canned response for flicker test (#12377)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-11-04 16:08:46 +00:00
Jacob MacDonald da4fa5ad75 Extensions MCP refactor (#12413) 2025-11-04 15:51:18 +00:00
anj-s 2b77c1ded4 SI prompt nudge for the todo tool (#12159) 2025-11-04 14:25:15 +00:00
Jainam M 6ab1b239ca refactor(core): Refactored and removed redundant test lines in telemetry (#12356) 2025-11-04 08:42:31 +00:00
Chris Coutinho ab73051298 fix(mcp): replace hardcoded port 7777 with dynamic port allocation for OAuth (#12520) 2025-11-04 07:16:56 +00:00
Edilmo Palencia cb2880cb93 feat(hooks): Hook Execution Planning and Matching (#9090) 2025-11-04 06:25:43 +00:00
Tommaso Sciortino b5315bfc20 Fix alt+left on ghostty (#12503) 2025-11-04 04:30:42 +00:00
Edilmo Palencia b259153403 feat(hooks): Hook Input/Output Contracts (#9080) 2025-11-04 02:47:23 +00:00
Sandy Tao f0c3c81e94 fix(core): Improve loop detection for longer repeating patterns (#12505) 2025-11-04 02:06:22 +00:00
christine betts fc42c4613f Only show screen reader notice once (#12247) 2025-11-04 02:04:48 +00:00
cornmander bd06e5b161 chore: bump vite to 7.1.12 (#12512) 2025-11-04 01:21:18 +00:00
Jacob Richman ad33c22374 Modify navigation and completion keyboard shortcuts to not use scroll. (#12502) 2025-11-04 00:22:04 +00:00
anthony bushong f3759381b1 feat(core): add timeout to llm edit fix (#12393) 2025-11-04 00:04:51 +00:00
Edilmo Palencia 80673a0c0c feat(hooks): Hook Type Decoupling and Translation (#9078) 2025-11-03 23:56:53 +00:00
Jacob Richman 1671bf77fb Alt buffer default (#12507) 2025-11-03 23:48:24 +00:00
Allen Hutchison ffc5e4d048 Refactor PolicyEngine to Core Package (#12325)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-03 23:41:00 +00:00
anj-s 60d2c2cc90 Enable WriteTodos tool by default (#12500) 2025-11-03 23:40:20 +00:00
Gal Zahavi 2144d25885 fix(auth): Return empty map if token file does not exits, and refacto… (#12332)
Co-authored-by: Sandy Tao <sandytao520@icloud.com>
2025-11-03 23:07:22 +00:00
Silvio Junior 7a515339ea Log recovery events (nudges) that happens inside the subagent (#12408) 2025-11-03 22:53:43 +00:00
Shreya Keshive be1dc13bb1 feat(core): Add support for listing experiments (#12495) 2025-11-03 21:51:22 +00:00
anj-s 1c044ba8af (fix): Respect ctrl+c signal for aborting execution in NonInteractive mode (#11478) 2025-11-03 21:49:01 +00:00
Jacob Richman 4fc9b1cde2 alternate buffer support (#12471) 2025-11-03 21:41:58 +00:00
Silvio Junior 60973aacd9 Grants subagent a recovery turn for when it hits TIMEOUT, MAX_TURNS or NO_TOOL_CALL failures. (#12344) 2025-11-03 21:22:12 +00:00
Silvio Junior 1c18552426 Enforce timeout for subagents (#12232) 2025-11-03 20:33:04 +00:00
Shehab c4377c1b1a fix(settings): persist restart-required changes when exiting with ESC (#12443) 2025-11-03 20:04:53 +00:00
Abhi 265f24e5d7 fix(ui): ensure model changes update the UI immediately (#12412) 2025-11-03 19:59:51 +00:00
Abhi 59e0b10e6c Cap Thinking Budget to prevent runaway thought loops (#12416) 2025-11-03 18:39:06 +00:00
Allen Hutchison 9d642f3bb1 refactor(core): improve error handling for setGlobalProxy (#12437) 2025-11-03 18:13:52 +00:00
Riddhi Dutta 19ea68b838 Refactoring packages/cli/src/ui tests (#12482)
Co-authored-by: riddhi <duttariddhi@google.com>
2025-11-03 18:10:57 +00:00
anj-s 93f14ce626 refactor: split core system prompt into multiple parts (#12461) 2025-11-03 17:08:56 +00:00
cornmander 1ef34261e0 chore: bump tar to 7.5.2 (#12466) 2025-11-03 07:36:35 +00:00
cornmander 5062fadf87 chore: autogenerate settings documentation (#12451) 2025-11-03 01:42:49 +00:00
cornmander 462c7d3502 feat(ui): add response semantic color (#12450)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: shambhu-hegde <143840542+shambhu-hegde@users.noreply.github.com>
2025-11-03 00:21:26 +00:00
Chris Coutinho 9187f6f6d1 fix: preserve path components in OAuth issuer URLs (#12448)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Gregory Shikhman <cornmander@cornmander.com>
2025-11-02 21:19:46 +00:00
Edilmo Palencia c0495ce2f9 feat(hooks): Hook Configuration Schema and Types (#9074)
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
2025-11-02 19:49:16 +00:00
Allen Hutchison 02518d2927 docs: update command-line flag documentation (#12452) 2025-11-02 17:59:03 +00:00
Jacob Richman d7243fb81f Add DarkGray to the ColorTheme. (#12420) 2025-11-01 23:50:51 +00:00
Eric Rahm e3262f8766 fix(core): combine .gitignore and .geminiignore logic for correct precedence (#11587)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2025-11-01 17:06:34 +00:00
Tommaso Sciortino caf2ca1438 Add kitty support for function keys. (#12415) 2025-10-31 23:19:55 +00:00
Jacob Richman f8ce3585eb Jacob314/jrichman ink (#12414) 2025-10-31 23:14:12 +00:00
Jacob Richman 61207fc2cb further incremental steps. Update the string width version to align with upstream ink (#12411) 2025-10-31 22:01:04 +00:00
Shreya Keshive b3cc397a31 feat(triage): overhaul automated issue triage workflow (#12365) 2025-10-31 21:21:05 +00:00
Shreya Keshive f9df415392 feat(core): Introduce release channel detection (#12257)
Co-authored-by: shishu314 <shishu_1998@yahoo.com>
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
Co-authored-by: Shardul Natu <43422294+kiranani@users.noreply.github.com>
Co-authored-by: Shnatu <snatu@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-31 21:18:39 +00:00
Abhi fd2cbacae9 fix(core): prevent model router from overriding explicit model choice (#12399) 2025-10-31 21:02:22 +00:00
Jacob Richman 8b93a5f27d I think the package lock was added in error to .gitignore. (#12405) 2025-10-31 20:29:42 +00:00
Jenna Inouye 9478bca67d Adding the Policy Engine docs to indexes. (#12404) 2025-10-31 20:23:56 +00:00
Jenna Inouye 4b53b3a6e6 Update telemetry.md to remove references to flags. (#12397) 2025-10-31 20:23:45 +00:00
Jacob Richman d72f8453cb Remove unused jsdom dep (#12394) 2025-10-31 20:03:57 +00:00
shishu314 6ee7165e39 feat(infra) - Add logging for slow rendering (#11147)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-10-31 19:51:05 +00:00
kevinjwang1 adddafe6d0 Handle untrusted folders on extension install and link (#12322)
Co-authored-by: Jacob MacDonald <jakemac@google.com>
2025-10-31 19:17:01 +00:00
shishu314 35f091bb01 feat(telemetry) - Add metric for slow rendering (#12391)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-10-31 18:57:47 +00:00
Sehoon Shon e9c7a80b12 migrate console.error to coreEvents for mcp-client-manager and google-auth-provider (#12342) 2025-10-31 18:52:56 +00:00
Sehoon Shon ab013fb7e9 migrating console.error to debugger for installationManager, oauth-provider, modifiable-tool (#12279) 2025-10-31 18:17:51 +00:00
Allen Hutchison c158923b27 docs: Add policy engine documentation and update sidebar (#12240) 2025-10-31 18:11:19 +00:00
Gaurav 31c5761e06 refactor: simplify daily quota error messages (#12386) 2025-10-31 17:49:32 +00:00
Jacob Richman f4ee245bf9 Switch to ink@. version 6.4.0 (#12381) 2025-10-31 16:40:51 +00:00
Tommaso Sciortino d13482e802 Mark model.compressionThreshold as requiring a restart (#12378) 2025-10-31 16:06:28 +00:00
Sandy Tao 11e1e98022 fix(core): ensure loop detection respects session disable flag (#12347) 2025-10-31 16:06:10 +00:00
Hriday b31f6804fe chore: migrate console.error to debugLogger in usePromptCompletion (#12208)
Co-authored-by: Hriday Taneja <hridayt@google.com>
2025-10-31 15:39:03 +00:00
Jerop Kipruto 01ad74a870 docs(cli): user.email attribute is only available for Google auth (#12372) 2025-10-31 15:13:38 +00:00
Jacob Richman f875911af7 Remove testing-library/react dep now that it is unused. (#12355) 2025-10-31 14:57:29 +00:00
Richard Michael f8ff921c42 Update mcp-server.md (#12310)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2025-10-31 14:46:51 +00:00
Jacob Richman ab8c24f5ea Fixes for Ink 6.4.0 (#12352) 2025-10-31 14:43:12 +00:00
Jerop Kipruto 9da3cb7e43 fix(core): remove duplicate session_id in GCP log exporter (#12370) 2025-10-31 14:42:13 +00:00
Jack Wotherspoon 6f69cdcc31 chore: make clear that --model is for choosing model on startup (#12367) 2025-10-31 14:01:56 +00:00
Shardul Natu 236334d015 feat(telemetry): Add extension name to ToolCallEvent telemetry (#12343)
Co-authored-by: Shnatu <snatu@google.com>
2025-10-31 13:50:22 +00:00
Jainam M 12472ce9b2 refactor(core): Refactored and removed redundant test lines in teleme… (#12284) 2025-10-31 07:33:33 +00:00
matt korwel e762cda55d fix: Address silent failure in release-patch-1-create-pr workflow (#12339) 2025-10-30 20:09:22 -07:00
matt korwel f566df91c8 feat: add dynamic run-names to patch release workflows (#12336) 2025-10-31 02:33:27 +00:00
gemini-cli-robot db37c715c0 chore/release: bump version to 0.13.0-nightly.20251031.c89bc30d (#12330) 2025-10-31 01:55:09 +00:00
Jacob Richman c89bc30d66 Code review script to catch common package/cli regressions (#12316)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-31 00:04:52 +00:00
Abhi 59e00eeda6 Remove context percentage in footer by default (#12326) 2025-10-30 23:18:25 +00:00
Tommaso Sciortino 3332703fca Make compression threshold editable in the UI. (#12317) 2025-10-30 23:03:58 +00:00
Victor May 643f2c0958 Enable model routing for all users (#12300) 2025-10-30 22:29:24 +00:00
Hyang-Ah Hana Kim c6a7107f78 fixing minor formatting issues in quota-and-pricing.md (#11340) 2025-10-30 21:46:23 +00:00
Sandy Tao 322feaafa6 refactor(core): decouple GeminiChat from uiTelemetryService via Usage events (#12196) 2025-10-30 21:05:38 +00:00
Tommaso Sciortino 68afb7200e Change default compression threshold (#12306) 2025-10-30 20:46:41 +00:00
matt korwel b382ae6803 feat: Prevent self-imports and fix build loop (#12309) 2025-10-30 20:15:49 +00:00
Jerop Kipruto 135d981e3c Create line change metrics (#12299) 2025-10-30 18:55:47 +00:00
Jacob Richman 54fa26ef0e Fix tests to wrap all calls changing the UI with act. (#12268) 2025-10-30 18:50:26 +00:00
Jacob MacDonald cc081337b7 Initial support for reloading extensions in the CLI - mcp servers only (#12239) 2025-10-30 18:05:49 +00:00
shishu314 d4cad0cdcc fix(test) - Make JSON output error test use canned response (#12250)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-10-30 17:25:01 +00:00
Pascal Birchler b8969cceff fix(docs): remove incorrect extension install method (#11194) 2025-10-30 17:24:48 +00:00
kevinjwang1 a3370ac86b Add validate command (#12186) 2025-10-30 16:08:13 +00:00
Jacob MacDonald 7d03151cd5 fix output messages for install and link (#12168) 2025-10-30 15:32:33 +00:00
matt korwel 054b4307ed Revert "fix(core): Add rootDir to tsconfig.json to resolve TS5055 error" (#12293) 2025-10-30 08:25:57 -07:00
Adib234 b8330b626e Fix misreported number of lines being removed by model (#12076) 2025-10-30 14:29:39 +00:00
christine betts 2a3244b1bb Log extension ID with tool call/slash command invocation (#12254)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-30 13:47:36 +00:00
Jenna Inouye 4d2a2de557 Docs: add v.0.11.0 to changelog (#12256) 2025-10-30 13:26:07 +00:00
Riddhi Dutta c583b510e0 Refactoring unit tests in packages/cli/src/ui (#12251)
Co-authored-by: riddhi <duttariddhi@google.com>
2025-10-30 07:18:45 +00:00
matt korwel 42c79c64d7 fix(core): Add rootDir to tsconfig.json to resolve TS5055 error (#12274) 2025-10-29 23:17:10 -07:00
Abhi 1d9e6870be feat(core): Implement granular memory loaders for JIT architecture (#12195) 2025-10-30 04:09:12 +00:00
Hriday 167b6ff8a1 chore: migrate console.error to debugLogger in useSlashCompletion (#12218)
Co-authored-by: Hriday Taneja <hridayt@google.com>
2025-10-30 03:59:15 +00:00
Jack Wotherspoon 77df6d48e2 docs: update keyboard shortcuts with missing shortcuts (#12024)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: cornmander <shikhman@google.com>
2025-10-30 02:39:15 +00:00
Gal Zahavi 06035d5d43 feat(auth): improve API key authentication flow (#11760)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-30 01:58:08 +00:00
Miguel Solorio 6c8a48db13 feat(ui): Fix Todo item text color not propagating for custom themes (#12265) 2025-10-29 23:39:31 +00:00
Sehoon Shon 523274dbf3 Standardize error logging with coreEvents.emitFeedback (#12199) 2025-10-29 22:27:17 +00:00
Shardul Natu 36207abec2 feat(telemetry): Add extensions to StartSessionEvent telemetry (#12261)
Co-authored-by: Shnatu <snatu@google.com>
2025-10-29 22:21:32 +00:00
shishu314 99f75f3218 Fix(noninteractive) - Add message when user uses deprecated flag (#11682)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-10-29 21:54:40 +00:00
Tommaso Sciortino 82c10421a0 Fix alt key mappings for mac (#12231) 2025-10-29 21:32:02 +00:00
Miguel Solorio 2e003ad8cf refactor(todo): improve performance and readability of todo component (#12238) 2025-10-29 21:19:44 +00:00
Tommaso Sciortino 42a265d290 Fix atprocessor test on windows (#12252) 2025-10-29 21:11:39 +00:00
Tommaso Sciortino 121732dde6 Hide collapsed Todo tray when they're all done. (#12242) 2025-10-29 20:41:20 +00:00
Shreya Keshive 3e9701861e refactor(core): Move getPackageJson utility to core package (#12224) 2025-10-29 20:23:35 +00:00
Gal Zahavi b31b786db7 refactor: Replace console.error with structured logging and feedback (#12175) 2025-10-29 20:20:11 +00:00
Shardul Natu 66e981ed1e feat: Add auth_type to StartSessionEvent OTel logging (#12034)
Co-authored-by: Shnatu <snatu@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-29 19:28:28 +00:00
shishu314 c60d8ef5a8 fix(infra) - Unskip read many file test (#12181)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-10-29 18:56:09 +00:00
shishu314 6e026bd950 fix(security) - Use emitFeedback instead of console error (#11954)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
2025-10-29 14:49:46 +00:00
gemini-cli-robot 372b588778 chore(release): bump version to 0.13.0-nightly.20251029.cca41edc (#12191)
Co-authored-by: matt korwel <matt.korwel@gmail.com>
2025-10-28 21:53:10 -07:00
Tommaso Sciortino 5d87a7f9c7 Remove Todo Icon (#12190) 2025-10-29 03:48:38 +00:00
lifefloating 706834ecd3 fix: enhance path handling in handleAtCommand to support relative paths (#9065)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2025-10-29 02:13:04 +00:00
701 changed files with 62933 additions and 21825 deletions
+140
View File
@@ -0,0 +1,140 @@
description="Reviews a pull request based on issue number."
prompt = """
Please provide a detailed pull request review on GitHub issue: {{args}}.
Follow these steps:
1. Use `gh pr view {{args}}` to pull the information of the PR.
2. Use `gh pr diff {{args}}` to view the diff of the PR.
3. Understand the intent of the PR using the PR description.
4. If PR description is not detailed enough to understand the intent of the PR,
make sure to note it in your review.
5. Make sure the PR title follows Conventional Commits, here are the last five
commits to the repo as examples: !{git log --pretty=format:"%s" -n 5}
6. Search the codebase if required.
7. Write a concise review of the PR, keeping in mind to encourage strong code
quality and best practices. Pay particular attention to the Gemini MD file
in the repo.
8. Consider ways the code may not be consistent with existing code in the repo.
In particular it is critical that the react code uses patterns consistent
with existing code in the repo.
9. Evaluate all tests on the PR and make sure that they are doing the following:
* Using `waitFor` from @{packages/cli/src/test-utils/async.ts} rather than
using `vi.waitFor` for all `waitFor` calls within `packages/cli`. Even if
tests pass, using the wrong `waitFor` could result in flaky tests as `act`
warnings could show up if timing is slightly different.
* Using `act` to wrap all blocks in tests that change component state.
* Using `toMatchSnapshot` to verify that rendering works as expected rather
than matching against the raw content of the output.
* If snapshots were changed as part of the pull request, review the snapshots
changes to ensure they are intentional and comment if any look at all
suspicious. Too many snapshot changes that indicate bugs have been approved
in the past.
* Use `render` or `renderWithProviders` from
@{packages/cli/src/test-utils/render.tsx} rather than using `render` from
`ink-testing-library` directly. This is needed to ensure that we do not get
warnings about spurious `act` calls. If test cases specify providers
directly, consider whether the existing `renderWithProviders` should be
modified to support that use case.
* Ensure the test cases are using parameterized tests where that might reduce
the number of duplicated lines significantly.
* NEVER use fixed waits (e.g. 'await delay(100)'). Always use 'waitFor' with
a predicate to ensure tests are stable and fast.
* Ensure mocks are properly managed:
* Critical dependencies (fs, os, child_process) should only be mocked at
the top of the file. Ideally avoid mocking these dependencies altogether.
* Check to see if there are existing mocks or fakes that can be used rather
than creating new ones for the new tests added.
* Try to avoid mocking the file system whenever possible. If using the real
file system is difficult consider whether the test should be an
integration test rather than a unit test.
* `vi.restoreAllMocks()` should be called in `afterEach` to prevent test
pollution.
* Use `vi.useFakeTimers()` for tests involving time-based logic to avoid
flakiness.
* Avoid using `any` in tests; prefer proper types or `unknown` with
narrowing.
* When creating parameterized tests, give the parameters types to ensure
that the tests are type-safe.
10. Evaluate all react logic carefully keeping in mind that the author of the PR
is not likely an expert on React. Key areas to audit carefully are:
* Whether `setState` calls trigger side effects from within the body of the
`setState` callback. If so, you *must* propose an alternate design using
reducers or other ways the code might be modified to not have to modify
state from within a `setState`. Make sure to comment about absolutely
every case like this as these cases have introduced multiple bugs in the
past. Typically these cases should be resolved using a reducer although
occassionally other techniques such as useRef are appropriate. Consider
suggesting that jacob314@ be tagged on the code review if the solution is
not 100% obvious.
* Whether code might introduce an infinite rendering loop in React.
* Whether keyboard handling is robust. Keyboard handling must go through
`useKeyPress.ts` from the Gemini CLI package rather than using the
standard ink library used by most keyboard handling. Unlike the standard
ink library, the keyboard handling library in Gemini CLI may report
multiple keyboard events one after another in the same React frame. This
is needed to support slow terminals but introduces complexity in all our
code that handles keyboard events. Handling this correctly often means
that reducers must be used or other mechanisms to ensure that multiple
state updates one after another are handled gracefully rather than
overriding values from the first update with the second update. Refer to
text-buffer.ts as a canonical example of using a reducer for this sort of
case.
* Ensure code does not use `console.log`, `console.warn`, or `console.error`
as these indicate debug logging that was accidentally left in the code.
* Avoid synchronous file I/O in React components as it will hang the UI.
* Ensure state initialization is explicit (e.g., use 'undefined' rather than
'true' as a default if the state is truly unknown initially).
* Carefully manage 'useEffect' dependencies. Prefer to use a reducer
whenever practical to resolve the issues. If that is not practical it is
ok to use 'useRef' to access the latest value of a prop or state inside an
effect without adding it to the dependency array if re-running the effect
is undesirable (common in event listeners).
* NEVER disable 'react-hooks/exhaustive-deps'. Fix the code to correctly
declare dependencies. Disabling this lint rule will almost always lead to
hard to detect bugs.
* Avoid making types nullable unless strictly necessary, as it hurts
readability.
* Do not introduce excessive property drilling. There are multiple providers
that can be leveraged to avoid property drilling. Make sure one of them
cannot be used. Do suggest a provider that might make sense to be extended
to include the new property or propose a new provider to add if the
property drilling is excessive. Only use providers for properties that are
consistent for the entire application.
11. General Gemini CLI design principles:
* Make sure that settings are only used for options that a user might
consider changing.
* Do not add new command line arguments and suggest settings instead.
* New settings must be added to packages/cli/src/config/settingsSchema.ts.
* If a setting has 'showInDialog: true', it MUST be documented in
docs/get-started/configuration.md.
* Ensure 'requiresRestart' is correctly set for new settings.
* Use 'debugLogger' for rethrown errors to avoid duplicate logging.
* All new keyboard shortcuts MUST be documented in
docs/cli/keyboard-shortcuts.md.
* Ensure new keyboard shortcuts are defined in
packages/cli/src/config/keyBindings.ts.
* If new keyboard shortcuts are added, remind the user to test them in
VSCode, iTerm2, Ghostty, and Windows to ensure they work for all
users.
* Be careful of keybindings that require the meta key as only certain
meta key shortcuts are supported on Mac.
* Be skeptical of function keys and keyboard shortcuts that are commonly
bound in VSCode as they may conflict.
12. TypeScript Best Practices:
* Use 'checkExhaustive' in the 'default' clause of 'switch' statements to
ensure all cases are handled.
* Avoid using the non-null assertion operator ('!') unless absolutely
necessary and you are confident the value is not null.
13. If the change might at all impact the prompts sent to Gemini CLI, flagged
that the change could impact Gemini CLI quality and make sure anj-s has been
tagged on the code review.
14. Discuss with me before making any comments on the issue. I will clarify
which possible issues you identified are problems, which ones you need to
investigate further, and which ones I do not care about.
15. If I request you to add comments to the issue, use
`gh pr comment {{args}} --body {{review}}` to post the review to the PR.
Remember to use the GitHub CLI (`gh`) with the Shell tool for all
GitHub-related tasks.
"""
+1 -1
View File
@@ -1,8 +1,8 @@
name: 'Bug Report'
description: 'Report a bug to help us improve Gemini CLI'
labels:
- 'kind/bug'
- 'status/need-triage'
type: 'Bug'
body:
- type: 'markdown'
attributes:
+1 -1
View File
@@ -1,8 +1,8 @@
name: 'Feature Request'
description: 'Suggest an idea for this project'
labels:
- 'kind/enhancement'
- 'status/need-triage'
type: 'Feature'
body:
- type: 'markdown'
attributes:
+1 -2
View File
@@ -1,8 +1,7 @@
name: 'Website issue'
description: 'Report an issue with the Gemini CLI Website and Gemini CLI Extensions Gallery'
labels:
- 'area/website'
- 'status/need-triage'
- 'area/extensions'
body:
- type: 'markdown'
attributes:
+7 -1
View File
@@ -90,6 +90,12 @@ jobs:
- name: 'Run Prettier'
run: 'node scripts/lint.js --prettier'
- name: 'Build docs prerequisites'
run: 'npm run predocs:settings'
- name: 'Verify settings docs'
run: 'npm run docs:settings -- --check'
- name: 'Run sensitive keyword linter'
run: 'node scripts/lint.js --sensitive-keywords'
@@ -102,7 +108,7 @@ jobs:
- name: 'Link Checker'
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
with:
args: '--verbose ./**/*.md'
args: '--verbose --accept 200,503 ./**/*.md'
fail: true
test_linux:
name: 'Test (Linux)'
@@ -45,7 +45,9 @@ jobs:
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')
))
)
)
) &&
!contains(github.event.issue.labels.*.name, 'area/') &&
!contains(github.event.issue.labels.*.name, 'priority/')
timeout-minutes: 5
runs-on: 'ubuntu-latest'
steps:
@@ -67,14 +69,24 @@ jobs:
core.setOutput('labels', issue.labels.map(label => label.name).join(','));
return issue;
- name: 'Check for triage label on manual trigger'
- name: 'Manual Trigger Pre-flight Checks'
if: |-
github.event_name == 'workflow_dispatch' && !contains(steps.get_issue_data.outputs.labels, 'status/need-triage')
github.event_name == 'workflow_dispatch'
env:
ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number }}'
LABELS: '${{ steps.get_issue_data.outputs.labels }}'
run: |
echo "Issue #${ISSUE_NUMBER_INPUT} does not have the 'status/need-triage' label. Stopping workflow."
exit 1
if ! echo "${LABELS}" | grep -q 'status/need-triage'; then
echo "Issue #${ISSUE_NUMBER_INPUT} does not have the 'status/need-triage' label. Stopping workflow."
exit 1
fi
if echo "${LABELS}" | grep -q 'area/' || echo "${LABELS}" | grep -q 'priority/'; then
echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' or 'priority/' labels. Stopping workflow."
exit 1
fi
echo "Manual triage checks passed."
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
@@ -97,7 +109,22 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
});
const labelNames = labels.map(label => label.name);
const allowedLabels = [
'priority/p0',
'priority/p1',
'priority/p2',
'priority/p3',
'priority/unknown',
'area/agent',
'area/enterprise',
'area/non-interactive',
'area/core',
'area/security',
'area/platform',
'area/extensions',
'area/unknown'
];
const labelNames = labels.map(label => label.name).filter(name => allowedLabels.includes(name));
core.setOutput('available_labels', labelNames.join(','));
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
return labelNames;
@@ -126,9 +153,6 @@ jobs:
settings: |-
{
"maxSessionTurns": 25,
"coreTools": [
"run_shell_command(echo)"
],
"telemetry": {
"enabled": true,
"target": "gcp"
@@ -137,128 +161,134 @@ jobs:
prompt: |-
## Role
You are an issue triage assistant. Analyze the current GitHub issue
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 the following labels titled maintainer, help wanted or good first issue.
You are an issue triage assistant. Your role is to analyze a GitHub issue and determine the single most appropriate area/ label and the single most appropriate priority/ label based on the definitions provided.
## Steps
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. 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 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"}
```
8. If the issue cannot be classified using the available labels, output:
```
{"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.
1. Review the issue title and body: ${{ env.ISSUE_TITLE }} and ${{ env.ISSUE_BODY }}.
2. Review the available labels: ${{ env.AVAILABLE_LABELS }}.
3. Select exactly one area/ label that best matches the issue based on Reference 1: Area Definitions.
4. Select exactly one priority/ label that best matches the issue based on Reference 2: Priority Definitions.
5. Fallback Logic:
- If you cannot confidently determine the correct area/ label from the definitions, you must use area/unknown.
- If you cannot confidently determine the correct priority/ label from the definitions, you must use priority/unknown.
6. Output your two selected labels in JSON format and nothing else. Example:
{"labels_to_set": ["area/core", "priority/p1"]}
## Guidelines
- Your output must contain exactly one area/ label and exactly one priority/ label.
- Triage only the current issue based on its title and body.
- Output only valid JSON format.
- 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
- Triage only the current issue
- Identify only one area/ label
- Identify only one kind/ label
- Identify all applicable sub-area/* and priority/* labels based on the issue content. It's ok to have multiple of these
- 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
- Reference all shell variables as "${VAR}" (with quotes and braces)
- Output only valid JSON format
- Do not include any explanation or additional text, just the JSON
Reference 1: Area Definitions
area/agent
- Description: Issues related to the "brain" of the CLI. This includes the core agent logic, model quality, tool/function calling, and memory.
- Example Issues:
"I am not getting a reasonable or expected response."
"The model is not calling the tool I expected."
"The web search tool is not working as expected."
"Feature request for a new built-in tool (e.g., read file, write file)."
"The generated code is poor quality or incorrect."
"The model seems stuck in a loop."
"The response from the model is malformed (e.g., broken JSON, bad formatting)."
"Concerns about unnecessary token consumption."
"Issues with how memory or chat history is managed."
"Issues with sub-agents."
"Model is switching from one to another unexpectedly."
Categorization Guidelines:
P0: Critical / Blocker
- 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.
- Crashes the application or makes a core feature completely unusable for all or most users in a production environment. Will it cause severe quality degration? Is it preventing contributors from contributing to the repository or is it a release blocker?
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.
- 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: 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:
- Affects a non-critical feature or a smaller, specific subset of users.
- An inconvenient but functional workaround is available and easy to execute.
- Noticeable UI/UX problems that don't break functionality but look unprofessional (e.g., elements are misaligned or overlapping).
Qualifier: Is it an annoying but non-blocking problem?
Example: An error message is unclear or contains a typo, causing user confusion but not halting their workflow.
P3: Low
- A P3 bug is a minor, low-impact issue that is trivial or cosmetic. It has little to no effect on the overall functionality of the application.
Impact:
- Minor cosmetic issues like color inconsistencies, typos in documentation, or slight alignment problems on a non-critical page.
- An edge-case bug that is very difficult to reproduce and affects a tiny fraction of users.
Qualifier: Is it a "nice-to-fix" issue?
Example: Spelling mistakes etc.
Things you should know:
- If users are talking about issues where the model gets downgraded from pro to flash then i want you to categorize that as a performance issue
- This product is designed to use different models eg.. using pro, downgrading to flash etc. when users report that they dont expect the model to change those would be categorized as feature requests.
Definition of Areas
area/ux:
- Issues concerning user-facing elements like command usability, interactive features, help docs, and perceived performance.
- I am seeing my screen flicker when using Gemini CLI
- I am seeing the output malformed
- Theme changes aren't taking effect
- My keyboard inputs arent' being recognzied
area/platform:
- Issues related to installation, packaging, OS compatibility (Windows, macOS, Linux), and the underlying CLI framework.
area/background: Issues related to long-running background tasks, daemons, and autonomous or proactive agent features.
area/models:
- i am not getting a response that is reasonable or expected. this can include things like
- I am calling a tool and the tool is not performing as expected.
- i am expecting a tool to be called and it is not getting called ,
- Including experience when using
- built-in tools (e.g., web search, code interpreter, read file, writefile, etc..),
- Function calling issues should be under this area
- i am getting responses from the model that are malformed.
- Issues concerning Gemini quality of response and inference,
- Issues talking about unnecessary token consumption.
- Issues talking about Model getting stuck in a loop be watchful as this could be the root cause for issues that otherwise seem like model performance issues.
- Memory compression
- unexpected responses,
- poor quality of generated code
area/tools:
- These are primarily issues related to Model Context Protocol
- These are issues that mention MCP support
- feature requests asking for support for new tools.
area/core: Issues with fundamental components like command parsing, configuration management, session state, and the main API client logic. Introducing multi-modality
area/contribution: Issues related to improving the developer contribution experience, such as CI/CD pipelines, build scripts, and test automation infrastructure.
area/authentication: Issues related to user identity, login flows, API key handling, credential storage, and access token management, unable to sign in selecting wrong authentication path etc..
area/security-privacy: Issues concerning vulnerability patching, dependency security, data sanitization, privacy controls, and preventing unauthorized data access.
area/extensibility: Issues related to the plugin system, extension APIs, or making the CLI's functionality available in other applications, github actions, ide support etc..
area/performance: Issues focused on model performance
- Issues with running out of capacity,
- 429 errors etc..
- could also pertain to latency,
- other general software performance like, memory usage, CPU consumption, and algorithmic efficiency.
- Switching models from one to the other unexpectedly.
area/enterprise
- Description: Issues specific to enterprise-level features, including telemetry, policy, and licenses.
- Example Issues:
"Usage data is not appearing in our telemetry dashboard."
"A user is able to perform an action that should be blocked by an admin policy."
"Questions about billing, licensing tiers, or enterprise quotas."
area/non-interactive
- Description: Issues related to using the CLI in automated or non-interactive environments (headless mode).
- Example Issues:
"Problems using the CLI as an SDK in another surface."
"The CLI is behaving differently when run from a shell script vs. an interactive terminal."
"GitHub action is failing."
"I am having trouble running the CLI in headless mode"
area/core
- Description: Issues with the fundamental CLI app itself. This includes the user interface (UI/UX), installation, OS compatibility, and performance.
- Example Issues:
"I am seeing my screen flicker when using the CLI."
"The output in my terminal is malformed or unreadable."
"Theme changes are not taking effect."
"Keyboard inputs (e.g., arrow keys, Ctrl+C) are not being recognized."
"The CLI failed to install or update."
"An issue specific to running on Windows, macOS, or Linux."
"Problems with command parsing, flags, or argument handling."
"High CPU or memory usage by the CLI process."
"Issues related to multi-modality (e.g., handling image inputs)."
"Problems with the IDE integration connection or installation"
area/security
- Description: Issues related to user authentication, authorization, data security, and privacy.
- Example Issues:
"I am unable to sign in."
"The login flow is selecting the wrong authentication path"
"Problems with API key handling or credential storage."
"A report of a security vulnerability"
"Concerns about data sanitization or potential data leaks."
"Issues or requests related to privacy controls."
"Preventing unauthorized data access."
area/platform
- Description: Issues related to CI/CD, release management, testing, eval infrastructure, capacity, quota management, and sandbox environments.
- Example Issues:
"I am getting a 429 'Resource Exhausted' or 500-level server error."
"General slowness or high latency from the service."
"The build script is broken on the main branch."
"Tests are failing in the CI/CD pipeline."
"Issues with the release management or publishing process."
"User is running out of capacity."
"Problems specific to the sandbox or staging environments."
"Questions about quota limits or requests for increases."
area/extensions
- Description: Issues related to the extension ecosystem, including the marketplace and website.
- Example Issues:
"Bugs related to the extension marketplace website."
"Issues with a specific extension."
"Feature request for the extension ecosystem."
area/unknown
- Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate.
Reference 2: Priority Definitions
priority/p0: Critical / Blocker
- Definition: A catastrophic failure that makes the CLI unusable for most users or poses a severe security risk. This includes installation failures, authentication failures, persistent crashes, or critical security vulnerabilities.
- Key Questions:
- Is the CLI failing to install or run?
- Does it fail to authenticate or connect to the Gemini API, making all commands useless?
- Is it consistently crashing on basic, common commands?
- Does this represent a critical security vulnerability?
priority/p1: High
- Definition: A severe issue where a core feature (e.g., text generation, code generation, file processing) is unusable, failing, has severe performance degradation, or providing fundamentally incorrect output formatting (e.g., truncated text, broken JSON). Affects many users, and there is no reasonable workaround.
- Key Questions:
- Is a core feature failing for a specific, large user group (e.g., all Windows users, all users of a specific shell)?
- Is the CLI failing to process a supported input or misinterpreting critical flags?
- Is the CLI's output formatting consistently broken, making the response unusable?
- Is a core command or feature extremely slow, making it impractical to use?
priority/p2: Medium
- Definition: A moderately impactful issue causing inconvenience or a non-optimal experience, but a reasonable workaround exists. This also includes failures in non-core features.
- Key Questions:
- Is a command or flag behaving incorrectly, but the user can achieve their goal via other means?
- Is there a significant, non-blocking UI/UX problem in the terminal (e.g., broken progress indicators, bad terminal coloring)?
priority/p3: Low
- Definition: A minor, low-impact issue with minimal effect on functionality. This includes most cosmetic defects, typos in documentation, or unclear help text.
- Key Questions:
- Is this a typo in the README.md, gemini --help text, or other documentation?
- Is this a minor cosmetic issue (e.g., text alignment in output, an extra newline) that doesn't affect usability?
priority/unknown
- Description: Issues that do not clearly fit into any other defined priority/ category, or where information is too limited to make a determination. Use this when no other priority is appropriate.
- name: 'Apply Labels to Issue'
if: |-
@@ -270,52 +300,61 @@ jobs:
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
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}`);
script: |
const rawOutput = process.env.LABELS_OUTPUT;
core.info(`Raw output from model: ${rawOutput}`);
let parsedLabels;
try {
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.");
// First, try to parse the raw output as JSON.
parsedLabels = JSON.parse(rawOutput);
} catch (jsonError) {
// If that fails, check for a markdown code block.
core.warning(`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`);
const jsonMatch = rawOutput.match(/```json\s*([\s\S]*?)\s*```/);
if (jsonMatch && jsonMatch[1]) {
try {
parsedLabels = JSON.parse(jsonMatch[1].trim());
} catch (markdownError) {
core.setFailed(`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawOutput}`);
return;
}
} else {
core.setFailed(`Output is not valid JSON and does not contain a JSON markdown block.\nRaw output: ${rawOutput}`);
return;
}
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}`);
return;
}
const issueNumber = parseInt(process.env.ISSUE_NUMBER);
const explanation = parsedLabels.explanation || '';
const labelsToSet = parsedLabels.labels_to_set || [];
labelsToSet.push('status/bot-triaged');
const labelsToAdd = parsedLabels.labels_to_set || [];
// Set labels based on triage result
if (labelsToSet.length > 0) {
await github.rest.issues.setLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToSet
});
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 explanationInfo = explanation ? ` - ${explanation}` : '';
core.info(`No labels to set for #${issueNumber}, leaving as is${explanationInfo}`);
if (labelsToAdd.length !== 2) {
core.setFailed(`Expected exactly 2 labels (one area/ and one priority/), but got ${labelsToAdd.length}. Labels: ${labelsToAdd.join(', ')}`);
return;
}
if (explanation) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: explanation,
});
// Set labels based on triage result
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToAdd
});
core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`);
// Remove the 'status/need-triage' label
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: 'status/need-triage'
});
core.info(`Successfully removed 'status/need-triage' label.`);
} catch (error) {
// If the label doesn't exist, the API call will throw a 404. We can ignore this.
if (error.status !== 404) {
core.warning(`Failed to remove 'status/need-triage': ${error.message}`);
}
}
- name: 'Post Issue Analysis Failure Comment'
+1 -1
View File
@@ -20,4 +20,4 @@ jobs:
id: 'lychee'
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
with:
args: '--verbose --no-progress ./**/*.md'
args: '--verbose --no-progress --accept 200,503 ./**/*.md'
@@ -1,5 +1,8 @@
name: 'Release: Patch (1) Create PR'
run-name: >-
Release Patch (1) Create PR | S:${{ inputs.channel }} | C:${{ inputs.commit }} ${{ inputs.original_pr && format('| PR:#{0}', inputs.original_pr) || '' }}
on:
workflow_dispatch:
inputs:
@@ -96,12 +99,12 @@ jobs:
--channel="${PATCH_CHANNEL}" \
--pullRequestNumber="${ORIGINAL_PR}" \
--dry-run="${DRY_RUN}"
echo "EXIT_CODE=$?" >> "$GITHUB_OUTPUT"
} 2>&1 | tee >(
echo "LOG_CONTENT<<EOF" >> "$GITHUB_ENV"
cat >> "$GITHUB_ENV"
echo "EOF" >> "$GITHUB_ENV"
)
echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
- name: 'Comment on Original PR'
if: 'always() && inputs.original_pr'
@@ -115,6 +118,7 @@ jobs:
GITHUB_RUN_ID: '${{ github.run_id }}'
LOG_CONTENT: '${{ env.LOG_CONTENT }}'
TARGET_REF: '${{ github.event.inputs.ref }}'
ENVIRONMENT: '${{ github.event.inputs.environment }}'
continue-on-error: true
run: |
git checkout "${TARGET_REF}"
@@ -1,5 +1,10 @@
name: 'Release: Patch (2) Trigger'
run-name: >-
Release Patch (2) Trigger |
${{ github.event.pull_request.number && format('PR #{0}', github.event.pull_request.number) || 'Manual' }} |
${{ github.event.pull_request.head.ref || github.event.inputs.ref }}
on:
pull_request:
types:
@@ -1,5 +1,8 @@
name: 'Release: Patch (3) Release'
run-name: >-
Release Patch (3) Release | T:${{ inputs.type }} | R:${{ inputs.release_ref }} ${{ inputs.original_pr && format('| PR:#{0}', inputs.original_pr) || '' }}
on:
workflow_dispatch:
inputs:
+5 -2
View File
@@ -92,6 +92,9 @@ jobs:
fi
NIGHTLY_COMMAND="node scripts/get-release-version.js --type=promote-nightly"
STABLE_JSON=$(${STABLE_COMMAND})
STABLE_VERSION=$(echo "${STABLE_JSON}" | jq -r .releaseVersion)
PREVIEW_COMMAND+=" --stable-base-version=${STABLE_VERSION}"
NIGHTLY_COMMAND+=" --stable-base-version=${STABLE_VERSION}"
PREVIEW_JSON=$(${PREVIEW_COMMAND})
NIGHTLY_JSON=$(${NIGHTLY_COMMAND})
echo "STABLE_JSON_COMMAND=${STABLE_COMMAND}"
@@ -100,14 +103,14 @@ jobs:
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}"
echo "STABLE_VERSION=${STABLE_VERSION}" >> "${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
REF="${REF_INPUT}"
SHA=$(git ls-remote origin "$REF" | awk '{print $1}')
SHA=$(git ls-remote origin "$REF" | awk -v ref="$REF" '$2 == "refs/heads/"ref || $2 == "refs/tags/"ref || $2 == ref {print $1}' | head -n 1)
if [ -z "$SHA" ]; then
if [[ "$REF" =~ ^[0-9a-f]{7,40}$ ]]; then
SHA="$REF"
-1
View File
@@ -17,7 +17,6 @@
# Dependency directory
node_modules
bower_components
package-lock.json
# Editors
.idea
+15 -4
View File
@@ -53,11 +53,22 @@ 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 your pull request involves changes to `packages/cli` (the frontend), we
recommend running our automated frontend review tool. **Note: This tool is
currently experimental.** It helps detect common React anti-patterns, testing
issues, and other frontend-specific best practices that are easy to miss.
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 run the review tool, enter the following command from within Gemini CLI:
```text
/review-frontend <PR_NUMBER>
```
Replace `<PR_NUMBER>` with your pull request number. Authors are encouraged to
run this on their own PRs for self-review, and reviewers should use it to
augment their manual review process.
### Self assigning issues
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
+1 -1
View File
@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+1
View File
@@ -4,6 +4,7 @@
[![Gemini CLI E2E](https://github.com/google-gemini/gemini-cli/actions/workflows/e2e.yml/badge.svg)](https://github.com/google-gemini/gemini-cli/actions/workflows/e2e.yml)
[![Version](https://img.shields.io/npm/v/@google/gemini-cli)](https://www.npmjs.com/package/@google/gemini-cli)
[![License](https://img.shields.io/github/license/google-gemini/gemini-cli)](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
<a href="https://codewiki.google/github.com/google-gemini/gemini-cli"><img src="https://www.gstatic.com/_/boq-sdlc-agents-ui/_/r/Mvosg4klCA4.svg" alt="Ask Code Wiki" height="20"></a>
![Gemini CLI Screenshot](./docs/assets/gemini-screenshot.png)
+152
View File
@@ -3,6 +3,158 @@
Wondering what's new in Gemini CLI? This document provides key highlights and
notable changes to Gemini CLI.
## v0.15.0 - Gemini CLI weekly update - 2025-11-03
- **🎉 Seamless scrollable UI & mouse support:** Weve given Gemini CLI a major
facelift to make your terminal experience smoother and much more polished. You
now get a flicker-free display with sticky headers that keep important context
visible and a stable input prompt that doesn't jump around. We even added
mouse support so you can click right where you need to type!
([gif](https://imgur.com/a/O6qc7bx),
[@jacob314](https://github.com/jacob314)).
- **Announcement:**
[https://developers.googleblog.com/en/making-the-terminal-beautiful-one-pixel-at-a-time/](https://developers.googleblog.com/en/making-the-terminal-beautiful-one-pixel-at-a-time/)
- **🎉 New partner extensions:**
- **Arize:** Seamlessly instrument AI applications with Arize AX and grant
direct access to Arize support:
`gemini extensions install https://github.com/Arize-ai/arize-tracing-assistant`
- **Chronosphere:** Retrieve logs, metrics, traces, events, and specific
entities:
`gemini extensions install https://github.com/chronosphereio/chronosphere-mcp`
- **Transmit:** Comprehensive context, validation, and automated fixes for
creating production-ready authentication and identity workflows:
`gemini extensions install https://github.com/TransmitSecurity/transmit-security-journey-builder`
- **Todo planning:** Complex questions now get broken down into todo lists that
the model can manage and check off. ([gif](https://imgur.com/a/EGDfNlZ),
[pr](https://github.com/google-gemini/gemini-cli/pull/12905) by
[@anj-s](https://github.com/anj-s))
- **Disable GitHub extensions:** Users can now prevent the installation and
loading of extensions from GitHub.
([pr](https://github.com/google-gemini/gemini-cli/pull/12838) by
[@kevinjwang1](https://github.com/kevinjwang1)).
- **Extensions restart:** Users can now explicitly restart extensions using the
`/extensions restart` command.
([pr](https://github.com/google-gemini/gemini-cli/pull/12739) by
[@jakemac53](https://github.com/jakemac53)).
- **Better Angular support:** Angular workflows should now be more seamless
([pr](https://github.com/google-gemini/gemini-cli/pull/10252) by
[@MarkTechson](https://github.com/MarkTechson)).
- **Validate command:** Users can now check that local extensions are formatted
correctly. ([pr](https://github.com/google-gemini/gemini-cli/pull/12186) by
[@kevinjwang1](https://github.com/kevinjwang1)).
## v0.12.0 - Gemini CLI weekly update - 2025-10-27
![Codebase investigator subagent in Gemini CLI.](https://i.imgur.com/4J1njsx.png)
- **🎉 New partner extensions:**
- **🤗 Hugging Face extension:** Access the Hugging Face hub.
([gif](https://drive.google.com/file/d/1LEzIuSH6_igFXq96_tWev11svBNyPJEB/view?usp=sharing&resourcekey=0-LtPTzR1woh-rxGtfPzjjfg))
`gemini extensions install https://github.com/huggingface/hf-mcp-server`
- **Monday.com extension**: Analyze your sprints, update your task boards,
etc.
([gif](https://drive.google.com/file/d/1cO0g6kY1odiBIrZTaqu5ZakaGZaZgpQv/view?usp=sharing&resourcekey=0-xEr67SIjXmAXRe1PKy7Jlw))
`gemini extensions install https://github.com/mondaycom/mcp`
- **Data Commons extension:** Query public datasets or ground responses on
data from Data Commons
([gif](https://drive.google.com/file/d/1cuj-B-vmUkeJnoBXrO_Y1CuqphYc6p-O/view?usp=sharing&resourcekey=0-0adXCXDQEd91ZZW63HbW-Q)).
`gemini extensions install https://github.com/gemini-cli-extensions/datacommons`
- **Model selection:** Choose the Gemini model for your session with `/model`.
([pic](https://imgur.com/a/ABFcWWw),
[pr](https://github.com/google-gemini/gemini-cli/pull/8940) by
[@abhipatel12](https://github.com/abhipatel12)).
- **Model routing:** Gemini CLI will now intelligently pick the best model for
the task. Simple queries will be sent to Flash while complex analytical or
creative tasks will still use the power of Pro. This ensures your quota will
last for a longer period of time. You can always opt-out of this via `/model`.
([pr](https://github.com/google-gemini/gemini-cli/pull/9262) by
[@abhipatel12](https://github.com/abhipatel12)).
- Discussion:
[https://github.com/google-gemini/gemini-cli/discussions/12375](https://github.com/google-gemini/gemini-cli/discussions/12375)
- **Codebase investigator subagent:** We now have a new built-in subagent that
will explore your workspace and resolve relevant information to improve
overall performance.
([pr](https://github.com/google-gemini/gemini-cli/pull/9988) by
[@abhipatel12](https://github.com/abhipatel12),
[pr](https://github.com/google-gemini/gemini-cli/pull/10282) by
[@silviojr](https://github.com/silviojr)).
- Enable, disable, or limit turns in `/settings`, plus advanced configs in
`settings.json` ([pic](https://imgur.com/a/yJiggNO),
[pr](https://github.com/google-gemini/gemini-cli/pull/10844) by
[@silviojr](https://github.com/silviojr)).
- **Explore extensions with `/extension`:** Users can now open the extensions
page in their default browser directly from the CLI using the `/extension`
explore command. ([pr](https://github.com/google-gemini/gemini-cli/pull/11846)
by [@JayadityaGit](https://github.com/JayadityaGit)).
- **Configurable compression:** Users can modify the compression threshold in
`/settings`. The default has been made more proactive
([pr](https://github.com/google-gemini/gemini-cli/pull/12317) by
[@scidomino](https://github.com/scidomino)).
- **API key authentication:** Users can now securely enter and store their
Gemini API key via a new dialog, eliminating the need for environment
variables and repeated entry.
([pr](https://github.com/google-gemini/gemini-cli/pull/11760) by
[@galz10](https://github.com/galz10)).
- **Sequential approval:** Users can now approve multiple tool calls
sequentially during execution.
([pr](https://github.com/google-gemini/gemini-cli/pull/11593) by
[@joshualitt](https://github.com/joshualitt)).
## v0.11.0 - Gemini CLI weekly update - 2025-10-20
![Gemini CLI and Jules](https://storage.googleapis.com/gweb-developer-goog-blog-assets/images/Jules_Extension_-_Blog_Header_O346JNt.original.png)
- 🎉 **Gemini CLI Jules Extension:** Use Gemini CLI to orchestrate Jules. Spawn
remote workers, delegate tedious tasks, or check in on running jobs!
- Install:
`gemini extensions install https://github.com/gemini-cli-extensions/jules`
- Announcement:
[https://developers.googleblog.com/en/introducing-the-jules-extension-for-gemini-cli/](https://developers.googleblog.com/en/introducing-the-jules-extension-for-gemini-cli/)
- **Stream JSON output:** Stream real-time JSONL events with
`--output-format stream-json` to monitor AI agent progress when run
headlessly. ([gif](https://imgur.com/a/0UCE81X),
[pr](https://github.com/google-gemini/gemini-cli/pull/10883) by
[@anj-s](https://github.com/anj-s))
- **Markdown toggle:** Users can now switch between rendered and raw markdown
display using `alt+m `or` ctrl+m`. ([gif](https://imgur.com/a/lDNdLqr),
[pr](https://github.com/google-gemini/gemini-cli/pull/10383) by
[@srivatsj](https://github.com/srivatsj))
- **Queued message editing:** Users can now quickly edit queued messages by
pressing the up arrow key when the input is empty.
([gif](https://imgur.com/a/ioRslLd),
[pr](https://github.com/google-gemini/gemini-cli/pull/10392) by
[@akhil29](https://github.com/akhil29))
- **JSON web fetch**: Non-HTML content like JSON APIs or raw source code are now
properly shown to the model (previously only supported HTML)
([gif](https://imgur.com/a/Q58U4qJ),
[pr](https://github.com/google-gemini/gemini-cli/pull/11284) by
[@abhipatel12](https://github.com/abhipatel12))
- **Non-interactive MCP commands:** Users can now run MCP slash commands in
non-interactive mode `gemini "/some-mcp-prompt"`.
([pr](https://github.com/google-gemini/gemini-cli/pull/10194) by
[@capachino](https://github.com/capachino))
- **Removal of deprecated flags:** Weve finally removed a number of deprecated
flags to cleanup Gemini CLIs invocation profile:
- `--all-files` / `-a` in favor of `@` from within Gemini CLI.
([pr](https://github.com/google-gemini/gemini-cli/pull/11228) by
[@allenhutchison](https://github.com/allenhutchison))
- `--telemetry-*` flags in favor of
[environment variables](https://github.com/google-gemini/gemini-cli/pull/11318)
([pr](https://github.com/google-gemini/gemini-cli/pull/11318) by
[@allenhutchison](https://github.com/allenhutchison))
## v0.10.0 - Gemini CLI weekly update - 2025-10-13
- **Polish:** The team has been heads down bug fixing and investing heavily into
+5 -15
View File
@@ -37,22 +37,12 @@ file in your project's temporary directory, typically located at
## Enabling the Feature
The Checkpointing feature is disabled by default. To enable it, you can either
use a command-line flag or edit your `settings.json` file.
The Checkpointing feature is disabled by default. To enable it, you need to edit
your `settings.json` file.
### Using the Command-Line Flag
You can enable checkpointing for the current session by using the
`--checkpointing` flag when starting the Gemini CLI:
```bash
gemini --checkpointing
```
### Using the `settings.json` File
To enable checkpointing by default for all sessions, you need to edit your
`settings.json` file.
> **Note:** The `--checkpointing` command-line flag was removed in version
> 0.11.0. Checkpointing can now only be enabled through the `settings.json`
> configuration file.
Add the following key to your `settings.json`:
+18 -6
View File
@@ -30,8 +30,9 @@ Slash commands provide meta-level control over the CLI itself.
checkpoints are:
- Linux/macOS: `~/.gemini/tmp/<project_hash>/`
- Windows: `C:\Users\<YourUsername>\.gemini\tmp\<project_hash>\`
- When you run `/chat list`, the CLI only scans these specific
directories to find available checkpoints.
- **Behavior:** Chats are saved into a project-specific directory,
determined by where you run the CLI. Consequently, saved chats are
only accessible when working within that same project.
- **Note:** These checkpoints are for manually saving and resuming
conversation states. For automatic checkpoints created before file
modifications, see the
@@ -39,8 +40,14 @@ Slash commands provide meta-level control over the CLI itself.
- **`resume`**
- **Description:** Resumes a conversation from a previous save.
- **Usage:** `/chat resume <tag>`
- **Note:** You can only resume chats that were saved within the current
project. To resume a chat from a different project, you must run the
Gemini CLI from that project's directory.
- **`list`**
- **Description:** Lists available tags for chat state resumption.
- **Note:** This command only lists chats saved within the current
project. Because chat history is project-scoped, chats saved in other
project directories will not be displayed.
- **`delete`**
- **Description:** Deletes a saved conversation checkpoint.
- **Usage:** `/chat delete <tag>`
@@ -121,6 +128,9 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Restarts all MCP servers and re-discovers their
available tools.
- [**`/model`**](./model.md)
- **Description:** Opens a dialog to choose your Gemini model.
- **`/memory`**
- **Description:** Manage the AI's instructional context (hierarchical memory
loaded from `GEMINI.md` files).
@@ -151,17 +161,19 @@ Slash commands provide meta-level control over the CLI itself.
edits made by a tool. If run without a tool call ID, it will list available
checkpoints to restore from.
- **Usage:** `/restore [tool_call_id]`
- **Note:** Only available if the CLI is invoked with the `--checkpointing`
option or configured via [settings](../get-started/configuration.md). See
- **Note:** Only available if checkpointing is configured via
[settings](../get-started/configuration.md). See
[Checkpointing documentation](../cli/checkpointing.md) for more details.
- **`/settings`**
- [**`/settings`**](./settings.md)
- **Description:** Open the settings editor to view and modify Gemini CLI
settings.
- **Details:** This command provides a user-friendly interface for changing
settings that control the behavior and appearance of Gemini CLI. It is
equivalent to manually editing the `.gemini/settings.json` file, but with
validation and guidance to prevent errors.
validation and guidance to prevent errors. See the
[settings documentation](./settings.md) for a full list of available
settings.
- **Usage:** Simply run `/settings` and the editor will open. You can then
browse or search for specific settings, view their current values, and
modify them as desired. Changes to some settings are applied immediately,
+3 -5
View File
@@ -494,6 +494,9 @@ the `excludedProjectEnvVars` setting in your `settings.json` file.
- **`GEMINI_SANDBOX`**:
- Alternative to the `sandbox` setting in `settings.json`.
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
- **`HTTP_PROXY` / `HTTPS_PROXY`**:
- Specifies the proxy server to use for outgoing HTTP/HTTPS requests.
- Example: `export HTTPS_PROXY="http://proxy.example.com:8080"`
- **`SEATBELT_PROFILE`** (macOS specific):
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
- `permissive-open`: (Default) Restricts writes to the project folder (and a
@@ -574,8 +577,6 @@ for that specific session.
- **`--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.
@@ -583,9 +584,6 @@ for that specific session.
- 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.
+2 -3
View File
@@ -363,9 +363,8 @@ containerized environment.
}
```
You can also specify a custom, hardened Docker image for the sandbox using the
`--sandbox-image` command-line argument or by building a custom
`sandbox.Dockerfile` as described in the
You can also specify a custom, hardened Docker image for the sandbox by building
a custom `sandbox.Dockerfile` as described in the
[Sandboxing documentation](./sandbox.md).
## Controlling Network Access via Proxy
+2 -2
View File
@@ -13,8 +13,8 @@ Git).
When you add a path to your `.geminiignore` file, tools that respect this file
will exclude matching files and directories from their operations. For example,
when you use the [`read_many_files`](../tools/multi-file.md) command, any paths
in your `.geminiignore` file will be automatically excluded.
when you use the `@` command to share files, any paths in your `.geminiignore`
file will be automatically excluded.
For the most part, `.geminiignore` follows the conventions of `.gitignore`
files:
+4 -1
View File
@@ -7,11 +7,14 @@ overview of Gemini CLI, see the [main documentation page](../index.md).
## Basic features
- **[Commands](./commands.md):** A reference for all built-in slash commands
(e.g., `/help`, `/chat`, `/tools`).
- **[Custom Commands](./custom-commands.md):** Create your own commands and
shortcuts for frequently used prompts.
- **[Headless Mode](./headless.md):** Use Gemini CLI programmatically for
scripting and automation.
- **[Model Selection](./model.md):** Configure the Gemini AI model used by the
CLI.
- **[Settings](./settings.md):** Configure various aspects of the CLI's behavior
and appearance.
- **[Themes](./themes.md):** Customizing the CLI's appearance with different
themes.
- **[Keyboard Shortcuts](./keyboard-shortcuts.md):** A reference for all
+131 -57
View File
@@ -1,69 +1,143 @@
# Gemini CLI Keyboard Shortcuts
This document lists the available keyboard shortcuts in the Gemini CLI.
Gemini CLI ships with a set of default keyboard shortcuts for editing input,
navigating history, and controlling the UI. Use this reference to learn the
available combinations.
## General
<!-- KEYBINDINGS-AUTOGEN:START -->
| Shortcut | Description |
| -------- | --------------------------------------------------------------------------------------------------------------------- |
| `Esc` | Close dialogs and suggestions. |
| `Ctrl+C` | Cancel the ongoing request and clear the input. Press twice to exit the application. |
| `Ctrl+D` | Exit the application if the input is empty. Press twice to confirm. |
| `Ctrl+L` | Clear the screen. |
| `Ctrl+S` | Allows long responses to print fully, disabling truncation. Use your terminal's scrollback to view the entire output. |
| `Ctrl+T` | Toggle the display of the todo list. |
| `Ctrl+Y` | Toggle auto-approval (YOLO mode) for all tool calls. |
| `F12` | Toggle the display of the debug console. |
#### Basic Controls
## Input Prompt
| Action | Keys |
| -------------------------------------------- | ------- |
| Confirm the current selection or choice. | `Enter` |
| Dismiss dialogs or cancel the current focus. | `Esc` |
| Shortcut | Description |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `!` | Toggle shell mode when the input is empty. |
| `\` (at end of line) + `Enter` | Insert a newline. |
| `Down Arrow` | Navigate down through the input history. |
| `Enter` | Submit the current prompt. |
| `Meta+Delete` / `Ctrl+Delete` | Delete the word to the right of the cursor. |
| `Tab` | Autocomplete the current suggestion if one exists. |
| `Up Arrow` | Navigate up through the input history. |
| `Ctrl+A` / `Home` | Move the cursor to the beginning of the line. |
| `Ctrl+B` / `Left Arrow` | Move the cursor one character to the left. |
| `Ctrl+C` | Clear the input prompt |
| `Esc` (double press) | Clear the input prompt. |
| `Ctrl+D` / `Delete` | Delete the character to the right of the cursor. |
| `Ctrl+E` / `End` | Move the cursor to the end of the line. |
| `Ctrl+F` / `Right Arrow` | Move the cursor one character to the right. |
| `Ctrl+H` / `Backspace` | Delete the character to the left of the cursor. |
| `Ctrl+K` | Delete from the cursor to the end of the line. |
| `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B` | Move the cursor one word to the left. |
| `Ctrl+N` | Navigate down through the input history. |
| `Ctrl+P` | Navigate up through the input history. |
| `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F` | Move the cursor one word to the right. |
| `Ctrl+U` | Delete from the cursor to the beginning of the line. |
| `Ctrl+V` | Paste clipboard content. If the clipboard contains an image, it will be saved and a reference to it will be inserted in the prompt. |
| `Ctrl+W` / `Meta+Backspace` / `Ctrl+Backspace` | Delete the word to the left of the cursor. |
| `Ctrl+X` / `Meta+Enter` | Open the current input in an external editor. |
#### Cursor Movement
## Suggestions
| Action | Keys |
| ----------------------------------------- | ---------------------- |
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home` |
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End` |
| Shortcut | Description |
| --------------- | -------------------------------------- |
| `Down Arrow` | Navigate down through the suggestions. |
| `Tab` / `Enter` | Accept the selected suggestion. |
| `Up Arrow` | Navigate up through the suggestions. |
#### Editing
## Radio Button Select
| Action | Keys |
| ------------------------------------------------ | ----------------------------------------- |
| Delete from the cursor to the end of the line. | `Ctrl + K` |
| Delete from the cursor to the start of the line. | `Ctrl + U` |
| Clear all text in the input field. | `Ctrl + C` |
| Delete the previous word. | `Ctrl + Backspace`<br />`Cmd + Backspace` |
| Shortcut | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
| `Down Arrow` / `j` | Move selection down. |
| `Enter` | Confirm selection. |
| `Up Arrow` / `k` | Move selection up. |
| `1-9` | Select an item by its number. |
| (multi-digit) | For items with numbers greater than 9, press the digits in quick succession to select the corresponding item. |
#### Screen Control
## IDE Integration
| Action | Keys |
| -------------------------------------------- | ---------- |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Shortcut | Description |
| -------- | --------------------------------- |
| `Ctrl+G` | See context CLI received from IDE |
#### Scrolling
| Action | Keys |
| ------------------------ | -------------------- |
| Scroll content up. | `Shift + Up Arrow` |
| Scroll content down. | `Shift + Down Arrow` |
| Scroll to the top. | `Home` |
| Scroll to the bottom. | `End` |
| Scroll up by one page. | `Page Up` |
| Scroll down by one page. | `Page Down` |
#### History & Search
| Action | Keys |
| -------------------------------------------- | --------------------- |
| Show the previous entry in history. | `Ctrl + P (no Shift)` |
| Show the next entry in history. | `Ctrl + N (no Shift)` |
| Start reverse search through history. | `Ctrl + R` |
| Insert the selected reverse-search match. | `Enter (no Ctrl)` |
| Accept a suggestion while reverse searching. | `Tab` |
#### Navigation
| Action | Keys |
| -------------------------------- | ------------------------------------------- |
| Move selection up in lists. | `Up Arrow (no Shift)` |
| Move selection down in lists. | `Down Arrow (no Shift)` |
| Move up within dialog options. | `Up Arrow (no Shift)`<br />`K (no Shift)` |
| Move down within dialog options. | `Down Arrow (no Shift)`<br />`J (no Shift)` |
#### Suggestions & Completions
| Action | Keys |
| --------------------------------------- | -------------------------------------------------- |
| Accept the inline suggestion. | `Tab`<br />`Enter (no Ctrl)` |
| Move to the previous completion option. | `Up Arrow (no Shift)`<br />`Ctrl + P (no Shift)` |
| Move to the next completion option. | `Down Arrow (no Shift)`<br />`Ctrl + N (no Shift)` |
| Expand an inline suggestion. | `Right Arrow` |
| Collapse an inline suggestion. | `Left Arrow` |
#### Text Input
| Action | Keys |
| ------------------------------------ | ------------------------------------------------------------------------------------------- |
| Submit the current prompt. | `Enter (no Ctrl, no Shift, no Cmd, not Paste)` |
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Paste + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
#### External Tools
| Action | Keys |
| ---------------------------------------------- | ---------- |
| Open the current prompt in an external editor. | `Ctrl + X` |
| Paste from the clipboard. | `Ctrl + V` |
#### App Controls
| Action | Keys |
| ----------------------------------------------------------------- | ---------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Toggle IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Cmd + M` |
| Toggle copy mode when the terminal is using the alternate buffer. | `Ctrl + S` |
| Expand a height-constrained response to show additional lines. | `Ctrl + S` |
| Toggle focus between the shell and Gemini input. | `Ctrl + F` |
#### Session Control
| Action | Keys |
| -------------------------------------------- | ---------- |
| Cancel the current request or quit the CLI. | `Ctrl + C` |
| Exit the CLI when the input buffer is empty. | `Ctrl + D` |
<!-- KEYBINDINGS-AUTOGEN:END -->
## Additional Context-Specific Shortcuts
- `Ctrl+Y`: Toggle YOLO (auto-approval) mode for tool calls.
- `Shift+Tab`: Toggle Auto Edit (auto-accept edits) mode.
- `Option+M` (macOS): Entering `µ` with Option+M also toggles Markdown
rendering, matching `Cmd+M`.
- `!` on an empty prompt: Enter or exit shell mode.
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
mode.
- `Ctrl+Delete` / `Meta+Delete`: Delete the word to the right of the cursor.
- `Ctrl+B` or `Left Arrow`: Move the cursor one character to the left while
editing text.
- `Ctrl+F` or `Right Arrow`: Move the cursor one character to the right; with an
embedded shell attached, `Ctrl+F` still toggles focus.
- `Ctrl+D` or `Delete`: Remove the character immediately to the right of the
cursor.
- `Ctrl+H` or `Backspace`: Remove the character immediately to the left of the
cursor.
- `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B`: Move one word to the left.
- `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F`: Move one word to the
right.
- `Ctrl+W`: Delete the word to the left of the cursor (in addition to
`Ctrl+Backspace` / `Cmd+Backspace`).
- `Ctrl+Z` / `Ctrl+Shift+Z`: Undo or redo the most recent text edit.
- `Meta+Enter`: Open the current input in an external editor (alias for
`Ctrl+X`).
- `Esc` pressed twice quickly: Clear the current input buffer.
- `Up Arrow` / `Down Arrow`: When the cursor is at the top or bottom of a
single-line input, navigate backward or forward through prompt history.
- `Number keys (1-9, multi-digit)` inside selection dialogs: Jump directly to
the numbered radio option and confirm when the full number is entered.
+56
View File
@@ -0,0 +1,56 @@
## Model Routing
Gemini CLI includes a model routing feature that automatically switches to a
fallback model in case of a model failure. This feature is enabled by default
and provides resilience when the primary model is unavailable.
## How it Works
Model routing is not based on prompt complexity, but is a fallback mechanism.
Here's how it works:
1. **Model Failure:** If the currently selected model fails to respond (for
example, due to a server error or other issue), the CLI will initiate the
fallback process.
2. **User Consent:** The CLI will prompt you to ask if you want to switch to
the fallback model. This is handled by the `fallbackModelHandler`.
3. **Fallback Activation:** If you consent, the CLI will activate the fallback
mode by calling `config.setFallbackMode(true)`.
4. **Model Switch:** On the next request, the CLI will use the
`DEFAULT_GEMINI_FLASH_MODEL` as the fallback model. This is handled by the
`resolveModel` function in
`packages/cli/src/zed-integration/zedIntegration.ts` which checks if
`isInFallbackMode()` is true.
## Configuration
Model routing is controlled by the `useModelRouter` setting in your
`settings.json` file.
- **`"experimental.useModelRouter": true` (Default):** Enables the model
routing/fallback feature.
- **`"experimental.useModelRouter": false`:** Disables the model
routing/fallback feature. If a model fails, the CLI will not attempt to switch
to a fallback model.
### Model Selection Precedence
The model used by Gemini CLI is determined by the following order of precedence:
1. **`--model` command-line flag:** A model specified with the `--model` flag
when launching the CLI will always be used.
2. **`GEMINI_MODEL` environment variable:** If the `--model` flag is not used,
the CLI will use the model specified in the `GEMINI_MODEL` environment
variable.
3. **`model.name` in `settings.json`:** If neither of the above are set, the
model specified in the `model.name` property of your `settings.json` file
will be used.
4. **Default Model:** If none of the above are set, the default model will be
used. The default model is determined by the `useModelRouter` setting:
- If `useModelRouter` is `true`, the default model is `"auto"`.
- If `useModelRouter` is `false`, the default model is the standard Gemini
model.
+58
View File
@@ -0,0 +1,58 @@
# Gemini CLI Model Selection (`/model` Command)
Select your Gemini CLI model. The `/model` command opens a dialog where you can
configure the model used by Gemini CLI, giving you more control over your
results.
## How to use the `/model` command
Use the following command in Gemini CLI:
```
/model
```
Running this command will open a dialog with your model options:
| Option | Description | Models |
| ------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Auto (recommended) | Let the system choose the best model for your task. | gemini-3-pro-preview (if enabled), gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite |
| Pro | For complex tasks that require deep reasoning and creativity. | gemini-3-pro-preview (if enabled), gemini-2.5-pro |
| Flash | For tasks that need a balance of speed and reasoning. | gemini-2.5-flash |
| Flash-Lite | For simple tasks that need to be done quickly. | gemini-2.5-flash-lite |
### Gemini 3 Pro and Preview Features
Note: Gemini 3 is not currently available on all account types. To learn more
about Gemini 3 access, refer to
[Gemini 3 Pro on Gemini CLI](../get-started/gemini-3.md).
To enable Gemini 3 Pro (if available), enable
[**Preview features** by using the `settings` command](../cli/settings.md). Once
enabled, Gemini CLI will attempt to use Gemini 3 Pro when you select **Auto** or
**Pro**. Both **Auto** and **Pro** will try to use Gemini 3 Pro before falling
back to Gemini 2.5 Pro.
You can also use the `--model` flag to specify a particular Gemini model on
startup. For more details, refer to the
[configuration documentation](./configuration.md).
Changes to these settings will be applied to all subsequent interactions with
Gemini CLI.
## Best practices for model selection
- **Default to Auto (recommended).** For most users, the _Auto (recommended)_
model provides a balance between speed and performance, automatically
selecting the correct model based on the complexity of the task. Example:
Developing a web application could include a mix of complex tasks (building
architecture and scaffolding the project) and simple tasks (generating CSS).
- **Switch to Pro if you aren't getting the results you want.** If you think you
need your model to be a little "smarter," use Pro. Pro will provide you with
the highest levels of reasoning and creativity. Example: A complex or
multi-stage debugging task.
- **Switch to Flash or Flash-Lite if you need faster results.** If you need a
simple response quickly, Flash or Flash-Lite is the best option. Example:
Converting a JSON object to a YAML string.
+113
View File
@@ -0,0 +1,113 @@
# Gemini CLI Settings (`/settings` Command)
Control your Gemini CLI experience with the `/settings` command. The `/settings`
command opens a dialog to view and edit all your Gemini CLI settings, including
your UI experience, keybindings, and accessibility features.
Your Gemini CLI settings are stored in a `settings.json` file. In addition to
using the `/settings` command, you can also edit them in one of the following
locations:
- **User settings**: `~/.gemini/settings.json`
- **Workspace settings**: `your-project/.gemini/settings.json`
Note: Workspace settings override user settings.
## Settings reference
Here is a list of all the available settings, grouped by category and ordered as
they appear in the UI.
### General
| UI Label | Setting | Description | Default |
| ------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------- | ----------- |
| Preview Features (e.g., models) | `general.previewFeatures` | Enable preview features (e.g., preview models). | `false` |
| Vim Mode | `general.vimMode` | Enable Vim keybindings. | `false` |
| Disable Auto Update | `general.disableAutoUpdate` | Disable automatic updates. | `false` |
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Session Retention | `general.sessionRetention` | Settings for automatic session cleanup. This feature is disabled by default. | `undefined` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup. | `false` |
### Output
| UI Label | Setting | Description | Default |
| ------------- | --------------- | ------------------------------------------------------ | ------- |
| Output Format | `output.format` | The format of the CLI output. Can be `text` or `json`. | `text` |
### UI
| UI Label | Setting | Description | Default |
| ------------------------------ | ---------------------------------------- | -------------------------------------------------------------------- | ------- |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar. | `false` |
| Show Status in Title | `ui.showStatusInTitle` | Show Gemini CLI status and thoughts in the terminal window title. | `false` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI. | `false` |
| Hide Banner | `ui.hideBanner` | Hide the application banner. | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI. | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI. | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `false` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Use Full Width | `ui.useFullWidth` | Use the entire width of the terminal for output. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `true` |
| Disable Loading Phrases | `ui.accessibility.disableLoadingPhrases` | Disable loading phrases for accessibility. | `false` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible. | `false` |
### IDE
| UI Label | Setting | Description | Default |
| -------- | ------------- | ---------------------------- | ------- |
| IDE Mode | `ide.enabled` | Enable IDE integration mode. | `false` |
### Model
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.2` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
### Context
| UI Label | Setting | Description | Default |
| ------------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Memory Discovery Max Dirs | `context.discoveryMaxDirs` | Maximum number of directories to search for memory. | `200` |
| Load Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
| Respect .gitignore | `context.fileFiltering.respectGitIgnore` | Respect .gitignore files when searching. | `true` |
| Respect .geminiignore | `context.fileFiltering.respectGeminiIgnore` | Respect .geminiignore files when searching. | `true` |
| Enable Recursive File Search | `context.fileFiltering.enableRecursiveFileSearch` | Enable recursive file search functionality when completing @ references in the prompt. | `true` |
| Disable Fuzzy Search | `context.fileFiltering.disableFuzzySearch` | Disable fuzzy search when searching for files. | `false` |
### Tools
| UI Label | Setting | Description | Default |
| -------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Auto Accept | `tools.autoAccept` | Automatically accept and execute tool calls that are considered safe (e.g., read-only operations). | `false` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `10000` |
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `100` |
| Enable Message Bus Integration | `tools.enableMessageBusIntegration` | Enable policy-based tool confirmation via message bus integration. | `false` |
### Security
| UI Label | Setting | Description | Default |
| -------------------------- | ------------------------------ | -------------------------------------------------- | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
### Experimental
| UI Label | Setting | Description | Default |
| ----------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ------- |
| Use Model Router | `experimental.useModelRouter` | Enable model routing to route requests to the best model based on complexity. | `true` |
| Enable Codebase Investigator | `experimental.codebaseInvestigatorSettings.enabled` | Enable the Codebase Investigator agent. | `true` |
| Codebase Investigator Max Num Turns | `experimental.codebaseInvestigatorSettings.maxNumTurns` | Maximum number of turns for the Codebase Investigator agent. | `10` |
+24 -16
View File
@@ -72,17 +72,17 @@ observability framework — Gemini CLI's observability system provides:
## Configuration
All telemetry behavior is controlled through your `.gemini/settings.json` file.
These settings can be overridden by environment variables or CLI flags.
Environment variables can be used to override the settings in the file.
| 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` |
| Setting | Environment Variable | Description | Values | Default |
| -------------- | -------------------------------- | ------------------------------------------------- | ----------------- | ----------------------- |
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
**Note on boolean environment variables:** For the boolean settings (`enabled`,
`logPrompts`, `useCollector`), setting the corresponding environment variable to
@@ -225,8 +225,9 @@ For local development and debugging, you can capture telemetry data locally:
The following section describes the structure of logs and metrics generated for
Gemini CLI.
The `session.id`, `installation.id`, and `user.email` are included as common
attributes on all logs and metrics.
The `session.id`, `installation.id`, and `user.email` (available only when
authenticated with a Google account) are included as common attributes on all
logs and metrics.
### Logs
@@ -251,6 +252,9 @@ Captures startup configuration and user prompt submissions.
- `debug_mode` (boolean)
- `mcp_servers` (string)
- `mcp_servers_count` (int)
- `extensions` (string)
- `extension_ids` (string)
- `extension_count` (int)
- `mcp_tools` (string, if applicable)
- `mcp_tools_count` (int, if applicable)
- `output_format` ("text", "json", or "stream-json")
@@ -278,6 +282,8 @@ Captures tool executions, output truncation, and Smart Edit behavior.
- `prompt_id` (string)
- `tool_type` ("native" or "mcp")
- `mcp_server_name` (string, if applicable)
- `extension_name` (string, if applicable)
- `extension_id` (string, if applicable)
- `content_length` (int, if applicable)
- `metadata` (if applicable)
@@ -539,10 +545,6 @@ Measures tool usage and latency.
- `decision` (string: "accept", "reject", "modify", or "auto_accept", if
applicable)
- `tool_type` (string: "mcp" or "native", if applicable)
- `model_added_lines` (Int, optional)
- `model_removed_lines` (Int, optional)
- `user_added_lines` (Int, optional)
- `user_removed_lines` (Int, optional)
- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency.
- **Attributes**:
@@ -586,6 +588,12 @@ Counts file operations with basic context.
- `extension` (string, optional)
- `programming_language` (string, optional)
- `gemini_cli.lines.changed` (Counter, Int): Number of lines changed (from file
diffs).
- **Attributes**:
- `function_name`
- `type` ("added" or "removed")
##### Chat and Streaming
Resilience counters for compression, invalid chunks, and retries.
+5
View File
@@ -88,6 +88,11 @@ color keys. For example:
- `DiffRemoved` (optional, for removed lines in diffs)
- `DiffModified` (optional, for modified lines in diffs)
You can also override individual UI text roles by adding a nested `text` object.
This object supports the keys `primary`, `secondary`, `link`, `accent`, and
`response`. When `text.response` is provided it takes precedence over
`text.primary` for rendering model responses in chat.
**Required Properties:**
- `name` (must match the key in the `customThemes` object and be a string)
+2
View File
@@ -11,6 +11,8 @@ requests sent from `packages/cli`. For a general overview of Gemini CLI, see the
registered, and used by the core.
- **[Memory Import Processor](./memport.md):** Documentation for the modular
GEMINI.md import feature using @file.md syntax.
- **[Policy Engine](./policy-engine.md):** Use the Policy Engine for
fine-grained control over tool execution.
## Role of the core
+246
View File
@@ -0,0 +1,246 @@
# Policy Engine
:::note This feature is currently in testing. To enable it, set
`tools.enableMessageBusIntegration` to `true` in your `settings.json` file. :::
The Gemini CLI includes a powerful policy engine that provides fine-grained
control over tool execution. It allows users and administrators to define rules
that determine whether a tool call should be allowed, denied, or require user
confirmation.
## Core concepts
The policy engine operates on a set of rules. Each rule is a combination of
conditions and a resulting decision. When a large language model wants to
execute a tool, the policy engine evaluates all rules to find the
highest-priority rule that matches the tool call.
A rule consists of the following main components:
- **Conditions**: Criteria that a tool call must meet for the rule to apply.
This can include the tool's name, the arguments provided to it, or the current
approval mode.
- **Decision**: The action to take if the rule matches (`allow`, `deny`, or
`ask_user`).
- **Priority**: A number that determines the rule's precedence. Higher numbers
win.
For example, this rule will ask for user confirmation before executing any `git`
command.
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git "
decision = "ask_user"
priority = 100
```
### Conditions
Conditions are the criteria that a tool call must meet for a rule to apply. The
primary conditions are the tool's name and its arguments.
#### Tool Name
The `toolName` in the rule must match the name of the tool being called.
- **Wildcards**: For Model-hosting-protocol (MCP) servers, you can use a
wildcard. A `toolName` of `my-server__*` will match any tool from the
`my-server` MCP.
#### Arguments Pattern
If `argsPattern` is specified, the tool's arguments are converted to a stable
JSON string, which is then tested against the provided regular expression. If
the arguments don't match the pattern, the rule does not apply.
### Decisions
There are three possible decisions a rule can enforce:
- `allow`: The tool call is executed automatically without user interaction.
- `deny`: The tool call is blocked and is not executed.
- `ask_user`: The user is prompted to approve or deny the tool call. (In
non-interactive mode, this is treated as `deny`.)
### Priority system & tiers
The policy engine uses a sophisticated priority system to resolve conflicts when
multiple rules match a single tool call. The core principle is simple: **the
rule with the highest priority wins**.
To provide a clear hierarchy, policies are organized into three tiers. Each tier
has a designated number that forms the base of the final priority calculation.
| Tier | Base | Description |
| :------ | :--- | :------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
| User | 2 | Custom policies defined by the user. |
| Admin | 3 | Policies managed by an administrator (e.g., in an enterprise environment). |
Within a TOML policy file, you assign a priority value from **0 to 999**. The
engine transforms this into a final priority using the following formula:
`final_priority = tier_base + (toml_priority / 1000)`
This system guarantees that:
- Admin policies always override User and Default policies.
- User policies always override Default policies.
- You can still order rules within a single tier with fine-grained control.
For example:
- A `priority: 50` rule in a Default policy file becomes `1.050`.
- A `priority: 100` rule in a User policy file becomes `2.100`.
- A `priority: 20` rule in an Admin policy file becomes `3.020`.
### Approval modes
Approval modes allow the policy engine to apply different sets of rules based on
the CLI's operational mode. A rule can be associated with one or more modes
(e.g., `yolo`, `autoEdit`). The rule will only be active if the CLI is running
in one of its specified modes. If a rule has no modes specified, it is always
active.
## Rule matching
When a tool call is made, the engine checks it against all active rules,
starting from the highest priority. The first rule that matches determines the
outcome.
A rule matches a tool call if all of its conditions are met:
1. **Tool Name**: The `toolName` in the rule must match the name of the tool
being called.
- **Wildcards**: For Model-hosting-protocol (MCP) servers, you can use a
wildcard. A `toolName` of `my-server__*` will match any tool from the
`my-server` MCP.
2. **Arguments Pattern**: If `argsPattern` is specified, the tool's arguments
are converted to a stable JSON string, which is then tested against the
provided regular expression. If the arguments don't match the pattern, the
rule does not apply.
## Configuration
Policies are defined in `.toml` files. The CLI loads these files from Default,
User, and (if configured) Admin directories.
### TOML rule schema
Here is a breakdown of the fields available in a TOML policy rule:
```toml
[[rule]]
# A unique name for the tool, or an array of names.
toolName = "run_shell_command"
# (Optional) The name of an MCP server. Can be combined with toolName
# to form a composite name like "mcpName__toolName".
mcpName = "my-custom-server"
# (Optional) A regex to match against the tool's arguments.
argsPattern = '"command":"(git|npm)'
# (Optional) A string or array of strings that a shell command must start with.
# This is syntactic sugar for `toolName = "run_shell_command"` and an `argsPattern`.
commandPrefix = "git "
# (Optional) A regex to match against the entire shell command.
# This is also syntactic sugar for `toolName = "run_shell_command"`.
# Note: This pattern is tested against the JSON representation of the arguments (e.g., `{"command":"<your_command>"}`), so anchors like `^` or `$` will apply to the full JSON string, not just the command text.
# You cannot use commandPrefix and commandRegex in the same rule.
commandRegex = "^git (commit|push)"
# The decision to take. Must be "allow", "deny", or "ask_user".
decision = "ask_user"
# The priority of the rule, from 0 to 999.
priority = 10
# (Optional) An array of approval modes where this rule is active.
modes = ["autoEdit"]
```
### Using arrays (lists)
To apply the same rule to multiple tools or command prefixes, you can provide an
array of strings for the `toolName` and `commandPrefix` fields.
**Example:**
This single rule will apply to both the `write_file` and `replace` tools.
```toml
[[rule]]
toolName = ["write_file", "replace"]
decision = "ask_user"
priority = 10
```
### Special syntax for `run_shell_command`
To simplify writing policies for `run_shell_command`, you can use
`commandPrefix` or `commandRegex` instead of the more complex `argsPattern`.
- `commandPrefix`: Matches if the `command` argument starts with the given
string.
- `commandRegex`: Matches if the `command` argument matches the given regular
expression.
**Example:**
This rule will ask for user confirmation before executing any `git` command.
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git "
decision = "ask_user"
priority = 100
```
### Special syntax for MCP tools
You can create rules that target tools from Model-hosting-protocol (MCP) servers
using the `mcpName` field or a wildcard pattern.
**1. Using `mcpName`**
To target a specific tool from a specific server, combine `mcpName` and
`toolName`.
```toml
# Allows the `search` tool on the `my-jira-server` MCP
[[rule]]
mcpName = "my-jira-server"
toolName = "search"
decision = "allow"
priority = 200
```
**2. Using a Wildcard**
To create a rule that applies to _all_ tools on a specific MCP server, specify
only the `mcpName`.
```toml
# Denies all tools from the `untrusted-server` MCP
[[rule]]
mcpName = "untrusted-server"
decision = "deny"
priority = 500
```
## Default policies
The Gemini CLI ships with a set of default policies to provide a safe
out-of-the-box experience.
- **Read-only tools** (like `read_file`, `glob`) are generally **allowed**.
- **Write tools** (like `write_file`, `run_shell_command`) default to
**`ask_user`**.
- In **`yolo`** mode, a high-priority rule allows all tools.
- In **`autoEdit`** mode, rules allow certain write operations to happen without
prompting.
+1 -2
View File
@@ -62,8 +62,7 @@ The core comes with a suite of pre-defined tools, typically found in
- **File System Tools:**
- `LSTool` (`ls.ts`): Lists directory contents.
- `ReadFileTool` (`read-file.ts`): Reads the content of a single file. It
takes an `absolute_path` parameter, which must be an absolute path.
- `ReadFileTool` (`read-file.ts`): Reads the content of a single file.
- `WriteFileTool` (`write-file.ts`): Writes content to a file.
- `GrepTool` (`grep.ts`): Searches for patterns in files.
- `GlobTool` (`glob.ts`): Finds files matching glob patterns.
+3 -5
View File
@@ -15,11 +15,9 @@ if you need to ship platform specific binary files.
This is the most flexible and simple option. All you need to do is 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.
can install your extension using `gemini extensions install <your-repo-uri>`.
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,
+34 -23
View File
@@ -42,16 +42,19 @@ installed on your machine. See
for help.
```
gemini extensions install https://github.com/gemini-cli-extensions/security
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent]
```
This will install the Gemini CLI Security extension, which offers support for a
`/security:analyze` command.
- `<source>`: The github URL or local path of the extension to install.
- `--ref`: The git ref to install from.
- `--auto-update`: Enable auto-update for this extension.
- `--pre-release`: Enable pre-release versions for this extension.
- `--consent`: Acknowledge the security risks of installing an extension and
skip the confirmation prompt.
### Uninstalling an extension
To uninstall, run `gemini extensions uninstall extension-name`, so, in the case
of the install example:
To uninstall, run `gemini extensions uninstall <name>`:
```
gemini extensions uninstall gemini-cli-security
@@ -62,27 +65,31 @@ gemini extensions uninstall gemini-cli-security
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.
```
gemini extensions disable <name> [--scope <scope>]
```
- `<name>`: The name of the extension to disable.
- `--scope`: The scope to disable the extension in (`user` or `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.
You can enable extensions using `gemini extensions enable <name>`. You can also
enable an extension for a specific workspace using
`gemini extensions enable <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.
```
gemini extensions enable <name> [--scope <scope>]
```
- `<name>`: The name of the extension to enable.
- `--scope`: The scope to enable the extension in (`user` or `workspace`).
### 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`.
`gemini-extension.json` `version` field) with `gemini extensions update <name>`.
You can update all extensions with:
@@ -90,10 +97,6 @@ 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`,
@@ -104,9 +107,12 @@ 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
gemini extensions new <path> [template]
```
- `<path>`: The path to create the extension in.
- `[template]`: The boilerplate template to use.
### Link a local extension
The `gemini extensions link` command will create a symbolic link from the
@@ -116,9 +122,11 @@ 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
gemini extensions link <path>
```
- `<path>`: The path of the extension to link.
## How it works
On startup, Gemini CLI looks for extensions in `<home>/.gemini/extensions`
@@ -179,6 +187,9 @@ precedence.
### Settings
_Note: This is an experimental feature. We do not yet recommend extension
authors introduce settings as part of their core flows._
Extensions can define settings that the user will be prompted to provide upon
installation. This is useful for things like API keys, URLs, or other
configuration that the extension needs to function.
+11 -1
View File
@@ -8,13 +8,23 @@ CLI, configure **one** of the following authentication methods:
- Use Gemini API key
- Use Vertex AI
- Headless (non-interactive) mode
- Google Cloud Shell
- Google Cloud Environments (Cloud Shell, Compute Engine, etc.)
## Quick Check: Running in Google Cloud Shell?
If you are running the Gemini CLI within a Google Cloud Shell environment,
authentication is typically automatic using your Cloud Shell credentials.
### Other Google Cloud Environments (e.g., Compute Engine)
Some other Google Cloud environments, such as Compute Engine VMs, might also
support automatic authentication. In these environments, Gemini CLI can
automatically use Application Default Credentials (ADC) sourced from the
environment's metadata server.
If automatic authentication does not occur in your environment, you will need to
use one of the interactive methods described below.
## Authenticate in Interactive mode
When you run Gemini CLI through the command-line, Gemini CLI will provide the
+3 -20
View File
@@ -473,21 +473,6 @@ a few things you can try in order of recommendation:
"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.
@@ -626,6 +611,9 @@ the `excludedProjectEnvVars` setting in your `settings.json` file.
- **`GEMINI_SANDBOX`**:
- Alternative to the `sandbox` setting in `settings.json`.
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
- **`HTTP_PROXY` / `HTTPS_PROXY`**:
- Specifies the proxy server to use for outgoing HTTP/HTTPS requests.
- Example: `export HTTPS_PROXY="http://proxy.example.com:8080"`
- **`SEATBELT_PROFILE`** (macOS specific):
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
- `permissive-open`: (Default) Restricts writes to the project folder (and a
@@ -707,8 +695,6 @@ for that specific session.
- **`--telemetry-log-prompts`**:
- Enables logging of prompts for telemetry. See
[telemetry](../cli/telemetry.md) for more information.
- **`--checkpointing`**:
- Enables [checkpointing](../cli/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.
@@ -716,9 +702,6 @@ for that specific session.
- 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.
+254 -122
View File
@@ -36,6 +36,11 @@ overridden by higher numbers):
Gemini CLI uses JSON settings files for persistent configuration. There are four
locations for these files:
> **Tip:** JSON-aware editors can use autocomplete and validation by pointing to
> the generated schema at `schemas/settings.schema.json` in this repository.
> When working outside the repo, reference the hosted schema at
> `https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json`.
- **System defaults file:**
- **Location:** `/etc/gemini-cli/system-defaults.json` (Linux),
`C:\ProgramData\gemini-cli\system-defaults.json` (Windows) or
@@ -89,18 +94,24 @@ contain other project-specific files related to Gemini CLI's operation, such as:
Settings are organized into categories. All settings should be placed within
their corresponding top-level category object in your `settings.json` file.
<!-- SETTINGS-AUTOGEN:START -->
#### `general`
- **`general.previewFeatures`** (boolean):
- **Description:** Enable preview features (e.g., preview models).
- **Default:** `false`
- **`general.preferredEditor`** (string):
- **Description:** The preferred editor to open files in.
- **Default:** `undefined`
- **`general.vimMode`** (boolean):
- **Description:** Enable Vim keybindings.
- **Description:** Enable Vim keybindings
- **Default:** `false`
- **`general.disableAutoUpdate`** (boolean):
- **Description:** Disable automatic updates.
- **Description:** Disable automatic updates
- **Default:** `false`
- **`general.disableUpdateNag`** (boolean):
@@ -108,34 +119,55 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **`general.checkpointing.enabled`** (boolean):
- **Description:** Enable session checkpointing for recovery.
- **Description:** Enable session checkpointing for recovery
- **Default:** `false`
- **Requires restart:** Yes
- **`general.enablePromptCompletion`** (boolean):
- **Description:** Enable AI-powered prompt completion suggestions while
typing.
- **Default:** `false`
- **Requires restart:** Yes
- **`general.retryFetchErrors`** (boolean):
- **Description:** Retry on "exception TypeError: fetch failed sending
request" errors.
- **Default:** `false`
- **`general.debugKeystrokeLogging`** (boolean):
- **Description:** Enable debug logging of keystrokes to the console.
- **Default:** `false`
- **`general.sessionRetention.enabled`** (boolean):
- **Description:** Enable automatic session cleanup.
- **Description:** Enable automatic session cleanup
- **Default:** `false`
- **`general.sessionRetention.maxAge`** (string):
- **Description:** Maximum age of sessions to keep (e.g., "30d", "7d", "24h",
"1w")
- **Default:** `undefined`
- **`general.sessionRetention.maxCount`** (number):
- **Description:** Alternative: Maximum number of sessions to keep (most
recent)
- **Default:** `undefined`
- **`general.sessionRetention.minRetention`** (string):
- **Description:** Minimum retention period (safety limit, defaults to "1d")
- **Default:** `"1d"`
#### `output`
- **`output.format`** (string):
- **`output.format`** (enum):
- **Description:** The format of the CLI output.
- **Default:** `"text"`
- **Values:** `"text"`, `"json"`, `"stream-json"`
- **Values:** `"text"`, `"json"`
#### `ui`
- **`ui.theme`** (string):
- **Description:** The color theme for the UI. See [Themes](../cli/themes.md)
for available options.
- **Description:** The color theme for the UI. See the CLI themes guide for
available options.
- **Default:** `undefined`
- **`ui.customThemes`** (object):
@@ -143,20 +175,21 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `{}`
- **`ui.hideWindowTitle`** (boolean):
- **Description:** Hide the window title bar.
- **Description:** Hide the window title bar
- **Default:** `false`
- **Requires restart:** Yes
- **`ui.showStatusInTitle`** (boolean):
- **Description:** Show Gemini CLI status and thoughts in the terminal window
title.
title
- **Default:** `false`
- **`ui.hideTips`** (boolean):
- **Description:** Hide helpful tips in the UI.
- **Description:** Hide helpful tips in the UI
- **Default:** `false`
- **`ui.hideBanner`** (boolean):
- **Description:** Hide the application banner.
- **Description:** Hide the application banner
- **Default:** `false`
- **`ui.hideContextSummary`** (boolean):
@@ -164,10 +197,6 @@ their corresponding top-level category object in your `settings.json` file.
input.
- **Default:** `false`
- **`ui.hideFooter`** (boolean):
- **Description:** Hide the footer from the UI.
- **Default:** `false`
- **`ui.footer.hideCWD`** (boolean):
- **Description:** Hide the current working directory path in the footer.
- **Default:** `false`
@@ -180,42 +209,69 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Hide the model name and context usage in the footer.
- **Default:** `false`
- **`ui.footer.hideContextPercentage`** (boolean):
- **Description:** Hides the context window remaining percentage.
- **Default:** `true`
- **`ui.hideFooter`** (boolean):
- **Description:** Hide the footer from the UI
- **Default:** `false`
- **`ui.showMemoryUsage`** (boolean):
- **Description:** Display memory usage information in the UI.
- **Description:** Display memory usage information in the UI
- **Default:** `false`
- **`ui.showLineNumbers`** (boolean):
- **Description:** Show line numbers in the chat.
- **Default:** `false`
- **Default:** `true`
- **`ui.showCitations`** (boolean):
- **Description:** Show citations for generated text in the chat.
- **Default:** `true`
- **Default:** `false`
- **`ui.showModelInfoInChat`** (boolean):
- **Description:** Show the model name in the chat for each model turn.
- **Default:** `false`
- **`ui.useFullWidth`** (boolean):
- **Description:** Use the entire width of the terminal for output.
- **Default:** `false`
- **Default:** `true`
- **`ui.useAlternateBuffer`** (boolean):
- **Description:** Use an alternate screen buffer for the UI, preserving shell
history.
- **Default:** `true`
- **Requires restart:** Yes
- **`ui.incrementalRendering`** (boolean):
- **Description:** Enable incremental rendering for the UI. This option will
reduce flickering but may cause rendering artifacts. Only supported when
useAlternateBuffer is enabled.
- **Default:** `true`
- **Requires restart:** Yes
- **`ui.customWittyPhrases`** (array):
- **Description:** Custom witty phrases to display during loading. When
provided, the CLI cycles through these instead of the defaults.
- **Default:** `[]`
- **`ui.accessibility.disableLoadingPhrases`** (boolean):
- **Description:** Disable loading phrases for accessibility.
- **Description:** Disable loading phrases for accessibility
- **Default:** `false`
- **Requires restart:** Yes
- **`ui.accessibility.screenReader`** (boolean):
- **Description:** Show plaintext interactive view that is more screen reader
friendly.
- **Description:** Render output in plain-text to be more screen reader
accessible
- **Default:** `false`
- **`ui.customWittyPhrases`** (array of strings):
- **Description:** A list of custom phrases to display during loading states.
When provided, the CLI will cycle through these phrases instead of the
default ones.
- **Default:** `[]`
- **Requires restart:** Yes
#### `ide`
- **`ide.enabled`** (boolean):
- **Description:** Enable IDE integration mode.
- **Description:** Enable IDE integration mode
- **Default:** `false`
- **Requires restart:** Yes
- **`ide.hasSeenNudge`** (boolean):
- **Description:** Whether the user has seen the IDE integration nudge.
@@ -224,8 +280,9 @@ their corresponding top-level category object in your `settings.json` file.
#### `privacy`
- **`privacy.usageStatisticsEnabled`** (boolean):
- **Description:** Enable collection of usage statistics.
- **Description:** Enable collection of usage statistics
- **Default:** `true`
- **Requires restart:** Yes
#### `model`
@@ -239,32 +296,40 @@ their corresponding top-level category object in your `settings.json` file.
- **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}}`
- **Description:** Enables or disables summarization of tool output. Configure
per-tool token budgets (for example {"run_shell_command": {"tokenBudget":
2000}}). Currently only the run_shell_command tool supports summarization.
- **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.compressionThreshold`** (number):
- **Description:** The fraction of context usage at which to trigger context
compression (e.g. 0.2, 0.3).
- **Default:** `0.5`
- **Requires restart:** Yes
- **`model.skipNextSpeakerCheck`** (boolean):
- **Description:** Skip the next speaker check.
- **Default:** `false`
- **`model.enableShellOutputEfficiency`** (boolean):
- **Description:** Optimizes shell tool commands for token efficiency.
- **Default:** `true`
#### `modelConfigs`
- **`modelConfigs.aliases`** (object):
- **Description:** Named presets for model configs. Can be used in place of a
model name and can inherit from other aliases using an `extends` property.
- **Default:**
`{"base":{"modelConfig":{"generateContentConfig":{"temperature":0,"topP":1}}},"chat-base":{"extends":"base","modelConfig":{"generateContentConfig":{"thinkingConfig":{"includeThoughts":true},"temperature":1,"topP":0.95,"topK":64}}},"chat-base-2.5":{"extends":"chat-base","modelConfig":{"generateContentConfig":{"thinkingConfig":{"thinkingBudget":8192}}}},"chat-base-3":{"extends":"chat-base","modelConfig":{"generateContentConfig":{"thinkingConfig":{"thinkingLevel":"HIGH"}}}},"gemini-3-pro-preview":{"extends":"chat-base-3","modelConfig":{"model":"gemini-3-pro-preview"}},"gemini-2.5-pro":{"extends":"chat-base-2.5","modelConfig":{"model":"gemini-2.5-pro"}},"gemini-2.5-flash":{"extends":"chat-base-2.5","modelConfig":{"model":"gemini-2.5-flash"}},"gemini-2.5-flash-lite":{"extends":"chat-base-2.5","modelConfig":{"model":"gemini-2.5-flash-lite"}},"gemini-2.5-flash-base":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash"}},"classifier":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"maxOutputTokens":1024,"thinkingConfig":{"thinkingBudget":512}}}},"prompt-completion":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"temperature":0.3,"maxOutputTokens":16000,"thinkingConfig":{"thinkingBudget":0}}}},"edit-corrector":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"thinkingConfig":{"thinkingBudget":0}}}},"summarizer-default":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"maxOutputTokens":2000}}},"summarizer-shell":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"maxOutputTokens":2000}}},"web-search":{"extends":"gemini-2.5-flash-base","modelConfig":{"generateContentConfig":{"tools":[{"googleSearch":{}}]}}},"web-fetch":{"extends":"gemini-2.5-flash-base","modelConfig":{"generateContentConfig":{"tools":[{"urlContext":{}}]}}},"web-fetch-fallback":{"extends":"gemini-2.5-flash-base","modelConfig":{}},"loop-detection":{"extends":"gemini-2.5-flash-base","modelConfig":{}},"loop-detection-double-check":{"extends":"base","modelConfig":{"model":"gemini-2.5-pro"}},"llm-edit-fixer":{"extends":"gemini-2.5-flash-base","modelConfig":{}},"next-speaker-checker":{"extends":"gemini-2.5-flash-base","modelConfig":{}}}`
- **`modelConfigs.overrides`** (array):
- **Description:** Apply specific configuration overrides based on matches,
with a primary key of model (or alias). The most specific match will be
used.
- **Default:** `[]`
#### `context`
- **`context.fileName`** (string or array of strings):
- **Description:** The name of the context file(s).
- **`context.fileName`** (string | string[]):
- **Description:** The name of the context file or files to load into memory.
Accepts either a single string or an array of strings.
- **Default:** `undefined`
- **`context.importFormat`** (string):
@@ -280,42 +345,51 @@ their corresponding top-level category object in your `settings.json` file.
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.
- **`context.loadMemoryFromIncludeDirectories`** (boolean):
- **Description:** Controls how /memory refresh loads GEMINI.md files. When
true, include directories are scanned; when false, only the current
directory is used.
- **Default:** `false`
- **`context.fileFiltering.respectGitIgnore`** (boolean):
- **Description:** Respect .gitignore files when searching.
- **Description:** Respect .gitignore files when searching
- **Default:** `true`
- **Requires restart:** Yes
- **`context.fileFiltering.respectGeminiIgnore`** (boolean):
- **Description:** Respect .geminiignore files when searching.
- **Description:** Respect .geminiignore files when searching
- **Default:** `true`
- **Requires restart:** Yes
- **`context.fileFiltering.enableRecursiveFileSearch`** (boolean):
- **Description:** Whether to enable searching recursively for filenames under
the current tree when completing `@` prefixes in the prompt.
- **Description:** Enable recursive file search functionality when completing
@ references in the prompt.
- **Default:** `true`
- **Requires restart:** Yes
- **`context.fileFiltering.disableFuzzySearch`** (boolean):
- **Description:** Disable fuzzy search when searching for files.
- **Default:** `false`
- **Requires restart:** Yes
#### `tools`
- **`tools.sandbox`** (boolean or string):
- **Description:** Sandbox execution environment (can be a boolean or a path
string).
- **`tools.sandbox`** (boolean | string):
- **Description:** Sandbox execution environment. Set to a boolean to enable
or disable the sandbox, or provide a string path to a sandbox profile.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`tools.shell.enableInteractiveShell`** (boolean):
- **Description:** Enables interactive terminal for running shell commands. If
an interactive session cannot be started, it will fall back to a standard
shell.
- **Description:** Use node-pty for an interactive shell experience. Fallback
to child_process still applies.
- **Default:** `true`
- **Requires restart:** Yes
- **`tools.shell.pager`** (string):
- **Description:** The pager command to use for shell output. Defaults to
`cat`.
- **Default:** `"cat"`
- **`tools.shell.showColor`** (boolean):
- **Description:** Show color in shell output.
@@ -326,42 +400,37 @@ their corresponding top-level category object in your `settings.json` file.
considered safe (e.g., read-only operations).
- **Default:** `false`
- **`tools.core`** (array of strings):
- **Description:** This can be used to restrict the set of built-in tools
[with an allowlist](../cli/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`.
- **`tools.core`** (array):
- **Description:** Restrict the set of built-in tools with an allowlist. Match
semantics mirror tools.allowed; see the built-in tools documentation for
available names.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`tools.exclude`** (array of strings):
- **`tools.allowed`** (array):
- **Description:** Tool names that bypass the confirmation dialog. Useful for
trusted commands (for example ["run_shell_command(git)",
"run_shell_command(npm test)"]). See shell tool command restrictions for
matching details.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`tools.exclude`** (array):
- **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`
- **Requires restart:** Yes
- **`tools.discoveryCommand`** (string):
- **Description:** Command to run for tool discovery.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`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 the 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).
- **Description:** Defines a custom shell command for invoking discovered
tools. The command must take the tool name as the first argument, read JSON
arguments from stdin, and emit JSON results on stdout.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`tools.useRipgrep`** (boolean):
- **Description:** Use ripgrep for file content search instead of the fallback
@@ -371,36 +440,51 @@ their corresponding top-level category object in your `settings.json` file.
- **`tools.enableToolOutputTruncation`** (boolean):
- **Description:** Enable truncation of large tool outputs.
- **Default:** `true`
- **Requires restart:** Yes
- **`tools.truncateToolOutputThreshold`** (number):
- **Description:** Truncate tool output if it is larger than this many
characters. Set to -1 to disable.
- **Default:** `20000`
- **Default:** `4000000`
- **Requires restart:** Yes
- **`tools.truncateToolOutputLines`** (number):
- **Description:** The number of lines to keep when truncating tool output.
- **Default:** `1000`
- **Requires restart:** Yes
- **`tools.enableMessageBusIntegration`** (boolean):
- **Description:** Enable policy-based tool confirmation via message bus
integration. When enabled, tools will automatically respect policy engine
integration. When enabled, tools automatically respect policy engine
decisions (ALLOW/DENY/ASK_USER) without requiring individual tool
implementations.
- **Default:** `false`
- **Requires restart:** Yes
- **`tools.enableHooks`** (boolean):
- **Description:** Enable the hooks system for intercepting and customizing
Gemini CLI behavior. When enabled, hooks configured in settings will execute
at appropriate lifecycle events (BeforeTool, AfterTool, BeforeModel, etc.).
Requires MessageBus integration.
- **Default:** `false`
- **Requires restart:** Yes
#### `mcp`
- **`mcp.serverCommand`** (string):
- **Description:** Command to start an MCP server.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`mcp.allowed`** (array of strings):
- **Description:** An allowlist of MCP servers to allow.
- **`mcp.allowed`** (array):
- **Description:** A list of MCP servers to allow.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`mcp.excluded`** (array of strings):
- **Description:** A denylist of MCP servers to exclude.
- **`mcp.excluded`** (array):
- **Description:** A list of MCP servers to exclude.
- **Default:** `undefined`
- **Requires restart:** Yes
#### `useSmartEdit`
@@ -412,37 +496,54 @@ their corresponding top-level category object in your `settings.json` file.
- **`useWriteTodos`** (boolean):
- **Description:** Enable the write_todos tool.
- **Default:** `false`
- **Default:** `true`
#### `security`
- **`security.disableYoloMode`** (boolean):
- **Description:** Disable YOLO mode, even if enabled by a flag.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.blockGitExtensions`** (boolean):
- **Description:** Blocks installing and loading extensions from Git.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.folderTrust.enabled`** (boolean):
- **Description:** Setting to track whether Folder trust is enabled.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.auth.selectedType`** (string):
- **Description:** The currently selected authentication type.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`security.auth.enforcedType`** (string):
- **Description:** The required auth type (useful for enterprises).
- **Description:** The required auth type. If this does not match the selected
auth type, the user will be prompted to re-authenticate.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`security.auth.useExternal`** (boolean):
- **Description:** Whether to use an external authentication flow.
- **Default:** `undefined`
- **Requires restart:** Yes
#### `advanced`
- **`advanced.autoConfigureMemory`** (boolean):
- **Description:** Automatically configure Node.js memory limits.
- **Description:** Automatically configure Node.js memory limits
- **Default:** `false`
- **Requires restart:** Yes
- **`advanced.dnsResolutionOrder`** (string):
- **Description:** The DNS resolution order.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`advanced.excludedEnvVars`** (array of strings):
- **`advanced.excludedEnvVars`** (array):
- **Description:** Environment variables to exclude from project context.
- **Default:** `["DEBUG","DEBUG_MODE"]`
@@ -452,10 +553,56 @@ their corresponding top-level category object in your `settings.json` file.
#### `experimental`
- **`experimental.extensionManagement`** (boolean):
- **Description:** Enable extension management features.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.extensionReloading`** (boolean):
- **Description:** Enables extension loading/unloading within the CLI session.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.useModelRouter`** (boolean):
- **Description:** Enable model routing to route requests to the best model
based on complexity.
- **Default:** `false`
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.enabled`** (boolean):
- **Description:** Enable the Codebase Investigator agent.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.maxNumTurns`** (number):
- **Description:** Maximum number of turns for the Codebase Investigator
agent.
- **Default:** `10`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.maxTimeMinutes`** (number):
- **Description:** Maximum time for the Codebase Investigator agent (in
minutes).
- **Default:** `3`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.thinkingBudget`** (number):
- **Description:** The thinking budget for the Codebase Investigator agent.
- **Default:** `8192`
- **Requires restart:** Yes
- **`experimental.codebaseInvestigatorSettings.model`** (string):
- **Description:** The model to use for the Codebase Investigator agent.
- **Default:** `"gemini-2.5-pro"`
- **Requires restart:** Yes
#### `hooks`
- **`hooks`** (object):
- **Description:** Hook configurations for intercepting and customizing agent
behavior.
- **Default:** `{}`
<!-- SETTINGS-AUTOGEN:END -->
#### `mcpServers`
@@ -736,15 +883,11 @@ for that specific session.
`--output-format json` or `--output-format stream-json` flag.
- **`--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.
- **`--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>`**:
@@ -760,22 +903,6 @@ for that specific session.
- A comma-separated list of tool names that will bypass the confirmation
dialog.
- Example: `gemini --allowed-tools "ShellTool(git status)"`
- **`--telemetry`**:
- Enables [telemetry](../cli/telemetry.md).
- **`--telemetry-target`**:
- Sets the telemetry target. See [telemetry](../cli/telemetry.md) for more
information.
- **`--telemetry-otlp-endpoint`**:
- Sets the OTLP endpoint for telemetry. See [telemetry](../cli/telemetry.md)
for more information.
- **`--telemetry-otlp-protocol`**:
- Sets the OTLP protocol for telemetry (`grpc` or `http`). Defaults to `grpc`.
See [telemetry](../cli/telemetry.md) for more information.
- **`--telemetry-log-prompts`**:
- Enables logging of prompts for telemetry. See
[telemetry](../cli/telemetry.md) for more information.
- **`--checkpointing`**:
- Enables [checkpointing](../cli/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.
@@ -783,9 +910,6 @@ for that specific session.
- 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.
@@ -798,6 +922,14 @@ for that specific session.
with screen readers.
- **`--version`**:
- Displays the version of the CLI.
- **`--experimental-acp`**:
- Starts the agent in ACP mode.
- **`--allowed-mcp-server-names`**:
- Allowed MCP server names.
- **`--fake-responses`**:
- Path to a file with fake model responses for testing.
- **`--record-responses`**:
- Path to a file to record model responses for testing.
## Context Files (Hierarchical Instructional Context)
+110
View File
@@ -0,0 +1,110 @@
# Gemini 3 Pro on Gemini CLI (Join the Waitlist)
Were excited to bring Gemini 3 Pro to Gemini CLI. For Google AI Ultra users
(Google AI Ultra for Business is not currently supported) and paid Gemini and
Vertex API key holders, Gemini 3 Pro is already available and ready to enable.
For everyone else, we're gradually expanding access
[through a waitlist](https://goo.gle/geminicli-waitlist-signup). Sign up for the
waitlist now to access Gemini 3 Pro once approved.
**Note:** Please wait until you have been approved to use Gemini 3 Pro to enable
**Preview Features**. If enabled early, the CLI will fallback to Gemini 2.5 Pro.
## Do I need to join the waitlist?
The following users will be **automatically granted access** to Gemini 3 Pro on
Gemini CLI:
- Google AI Ultra subscribers (excluding Google AI Ultra for Business, which is
on the roadmap).
- Gemini API key users
[with access to Gemini 3](https://ai.google.dev/gemini-api/docs/rate-limits).
- Vertex API key users
[with access to Gemini 3](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/quotas).
For **Gemini Code Assist Enterprise users**, access is coming soon.
Users not automatically granted access through one of these account types will
need to join the waitlist. This includes Google AI Pro, Gemini Code Assist
standard, and free tier users.
Note: Whether youre automatically granted access or accepted from the waitlist,
youll still need to enable Gemini 3 Pro
[using the `/settings` command](../cli/settings.md).
## How to join the waitlist
Users not automatically granted access will need to join the waitlist. Follow
these instructions to sign up:
- Install Gemini CLI.
- Authenticate using the **Login with Google** option. Youll see a banner that
says “Gemini 3 is now available.” If you do not see this banner, update your
installation of Gemini CLI to the most recent version.
- Fill out this Google form:
[Access Gemini 3 in Gemini CLI](https://goo.gle/geminicli-waitlist-signup).
Provide the email address of the account you used to authenticate with Gemini
CLI.
Users will be onboarded in batches, subject to availability. When youve been
granted access to Gemini 3 Pro, youll receive an acceptance email to your
submitted email address.
## How to use Gemini 3 Pro with Gemini CLI
Once you receive your acceptance emailor if you are automatically granted
accessyou still need to enable Gemini 3 Pro within Gemini CLI.
To enable Gemini 3 Pro, use the `/settings` command in Gemini CLI and set
**Preview Features** to `true`.
For more information, see [Gemini CLI Settings](../cli/settings.md).
### Usage limits and fallback
Gemini CLI will tell you when you reach your Gemini 3 Pro daily usage limit.
When you encounter that limit, youll be given the option to switch to Gemini
2.5 Pro, upgrade for higher limits, or stop. Youll also be told when your usage
limit resets and Gemini 3 Pro can be used again.
Similarly, when you reach your daily usage limit for Gemini 2.5 Pro, youll see
a message prompting fallback to Gemini 2.5 Flash.
### Capacity errors
There may be times when the Gemini 3 Pro model is overloaded. When that happens,
Gemini CLI will ask you to decide whether you want to keep trying Gemini 3 Pro
or fallback to Gemini 2.5 Pro.
**Note:** The **Keep trying** option uses exponential backoff, in which Gemini
CLI waits longer between each retry, when the system is busy. If the retry
doesn't happen immediately, please wait a few minutes for the request to
process.
## Model selection & routing types
When using Gemini CLI, you may want to control how your requests are routed
between models. By default, Gemini CLI uses **Auto** routing.
When using Gemini 3 Pro, you may want to use Auto routing or Pro routing to
manage your usage limits:
- **Auto routing:** Auto routing first determines whether a prompt involves a
complex or simple operation. For simple prompts, it will automatically use
Gemini 2.5 Flash. For complex prompts, if Gemini 3 Pro is enabled, it will use
Gemini 3 Pro; otherwise, it will use Gemini 2.5 Pro.
- **Pro routing:** If you want to ensure your task is processed by the most
capable model, use `/model` and select **Pro**. Gemini CLI will prioritize the
most capable model available, including Gemini 3 Pro if it has been enabled.
To learn more about selecting a model and routing, refer to
[Gemini CLI Model Selection](../cli/model.md).
## Need help?
If you need help, we recommend searching for an existing
[GitHub issue](https://github.com/google-gemini/gemini-cli/issues). If you
cannot find a GitHub issue that matches your concern, you can
[create a new issue](https://github.com/google-gemini/gemini-cli/issues/new/choose).
For comments and feedback, consider opening a
[GitHub discussion](https://github.com/google-gemini/gemini-cli/discussions).
+1
View File
@@ -63,3 +63,4 @@ To explore the power of Gemini CLI, see [Gemini CLI examples](./examples.md).
- Find out more about [Gemini CLI's tools](../tools/index.md).
- Review [Gemini CLI's commands](../cli/commands.md).
- Learn how to [get started with Gemini 3](./gemini-3.md).
+3 -3
View File
@@ -4,8 +4,8 @@ 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
Currently, the supported IDEs are [Antigravity](https://antigravity.google),
[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).
@@ -191,7 +191,7 @@ messages and how to resolve them.
- **Cause:** You are running Gemini CLI in a terminal or environment that is
not a supported IDE.
- **Solution:** Run Gemini CLI from the integrated terminal of a supported
IDE, like VS Code.
IDE, like Antigravity or VS Code.
- **Message:**
`No installer is available for IDE. Please install the Gemini CLI Companion extension manually from the marketplace.`
+8 -2
View File
@@ -28,12 +28,18 @@ This documentation is organized into the following sections:
- **[Configuration](./get-started/configuration.md):** Information on
configuring the CLI.
- **[Examples](./get-started/examples.md):** Example usage of Gemini CLI.
- **[Get started with Gemini 3](./get-started/gemini-3.md):** Learn how to
enable and use Gemini 3.
### CLI
- **[CLI overview](./cli/index.md):** Overview of the command-line interface.
- **[Commands](./cli/commands.md):** Description of available CLI commands.
- **[Enterprise](./cli/enterprise.md):** Gemini CLI for enterprise.
- **[Model Selection](./cli/model.md):** Select the model used to process your
commands with `/model`.
- **[Settings](./cli/settings.md):** Configure various aspects of the CLI's
behavior and appearance with `/settings`.
- **[Themes](./cli/themes.md):** Themes for Gemini CLI.
- **[Token Caching](./cli/token-caching.md):** Token caching and optimization.
- **[Tutorials](./cli/tutorials.md):** Tutorials for Gemini CLI.
@@ -50,6 +56,8 @@ This documentation is organized into the following sections:
- **[Memport](./core/memport.md):** Using the Memory Import Processor.
- **[Tools API](./core/tools-api.md):** Information on how the core manages and
exposes tools.
- **[Policy Engine](./core/policy-engine.md):** Use the Policy Engine for
fine-grained control over tool execution.
### Tools
@@ -58,8 +66,6 @@ This documentation is organized into the following sections:
- **[File System Tools](./tools/file-system.md):** Documentation for the
`read_file` and `write_file` tools.
- **[MCP servers](./tools/mcp-server.md):** Using MCP servers with Gemini CLI.
- **[Multi-File Read Tool](./tools/multi-file.md):** Documentation for the
`read_many_files` tool.
- **[Shell Tool](./tools/shell.md):** Documentation for the `run_shell_command`
tool.
- **[Web Fetch Tool](./tools/web-fetch.md):** Documentation for the `web_fetch`
+3
View File
@@ -72,6 +72,9 @@ these responses.
REGENERATE_MODEL_GOLDENS="true" npm run test:e2e
```
**WARNING**: Make sure you run **await rig.cleanup()** at the end of your test,
else the golden files will not be updated.
### Deflaking a test
Before adding a **new** integration test, you should test it at least 5 times
+8 -6
View File
@@ -8,8 +8,10 @@ account you use to authenticate.
See [privacy and terms](./tos-privacy.md) for details on Privacy policy and
Terms of Service.
> [!NOTE] Published prices are list price; additional negotiated commercial
> discounting may apply.
> [!NOTE]
>
> Published prices are list price; additional negotiated commercial discounting
> may apply.
This article outlines the specific quotas and pricing applicable to the Gemini
CLI when using different authentication methods.
@@ -82,10 +84,10 @@ Gemini CLI by upgrading to one of the following subscriptions:
[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.
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:
+16 -4
View File
@@ -23,6 +23,10 @@
"label": "Gemini CLI Quickstart",
"slug": "docs/get-started"
},
{
"label": "Gemini 3 Pro on Gemini CLI",
"slug": "docs/get-started/gemini-3"
},
{
"label": "Authentication",
"slug": "docs/get-started/authentication"
@@ -72,10 +76,18 @@
"label": "Keyboard Shortcuts",
"slug": "docs/cli/keyboard-shortcuts"
},
{
"label": "Model Selection",
"slug": "docs/cli/model"
},
{
"label": "Sandbox",
"slug": "docs/cli/sandbox"
},
{
"label": "Settings",
"slug": "docs/cli/settings"
},
{
"label": "Telemetry",
"slug": "docs/cli/telemetry"
@@ -116,6 +128,10 @@
{
"label": "Memory Import Processor",
"slug": "docs/core/memport"
},
{
"label": "Policy Engine",
"slug": "docs/core/policy-engine"
}
]
},
@@ -130,10 +146,6 @@
"label": "File System",
"slug": "docs/tools/file-system"
},
{
"label": "Multi-File Read",
"slug": "docs/tools/multi-file"
},
{
"label": "Shell",
"slug": "docs/tools/shell"
-2
View File
@@ -82,8 +82,6 @@ Gemini CLI's built-in tools can be broadly categorized as follows:
from URLs.
- **[Web Search Tool](./web-search.md) (`google_web_search`):** For searching
the web.
- **[Multi-File Read Tool](./multi-file.md) (`read_many_files`):** A specialized
tool for reading content from multiple files or directories.
- **[Memory Tool](./memory.md) (`save_memory`):** For saving and recalling
information across sessions.
- **[Todo Tool](./todos.md) (`write_todos`):** For managing subtasks of complex
+9 -4
View File
@@ -927,13 +927,13 @@ This is the default transport for running local servers.
```bash
# Basic syntax
gemini mcp add <name> <command> [args...]
gemini mcp add [options] <name> <command> [args...]
# Example: Adding a local server
gemini mcp add -e API_KEY=123 my-stdio-server /path/to/server arg1 arg2 arg3
gemini mcp add -e API_KEY=123 -e DEBUG=true my-stdio-server /path/to/server arg1 arg2 arg3
# Example: Adding a local python server
gemini mcp add python-server python server.py --port 8080
gemini mcp add python-server python server.py -- --server-arg my-value
```
#### Adding an HTTP server
@@ -969,7 +969,8 @@ gemini mcp add --transport sse --header "Authorization: Bearer abc123" secure-ss
### Listing Servers (`gemini mcp list`)
To view all MCP servers currently configured, use the `list` command. It
displays each server's name, configuration details, and connection status.
displays each server's name, configuration details, and connection status. This
command has no flags.
**Command:**
@@ -996,6 +997,10 @@ server's name.
gemini mcp remove <name>
```
**Options (Flags):**
- `-s, --scope`: Configuration scope (user or project). [default: "project"]
**Example:**
```bash
-109
View File
@@ -1,109 +0,0 @@
# Multi File Read Tool (`read_many_files`)
This document describes the `read_many_files` tool for the Gemini CLI.
## Description
Use `read_many_files` to read content from multiple files specified by paths or
glob patterns. The behavior of this tool depends on the provided files:
- For text files, this tool concatenates their content into a single string.
- For image (e.g., PNG, JPEG), PDF, audio (MP3, WAV), and video (MP4, MOV)
files, it reads and returns them as base64-encoded data, provided they are
explicitly requested by name or extension.
`read_many_files` can be used to perform tasks such as getting an overview of a
codebase, finding where specific functionality is implemented, reviewing
documentation, or gathering context from multiple configuration files.
**Note:** `read_many_files` looks for files following the provided paths or glob
patterns. A directory path such as `"/docs"` will return an empty result; the
tool requires a pattern such as `"/docs/*"` or `"/docs/*.md"` to identify the
relevant files.
### Arguments
`read_many_files` takes the following arguments:
- `paths` (list[string], required): An array of glob patterns or paths relative
to the tool's target directory (e.g., `["src/**/*.ts"]`,
`["README.md", "docs/*", "assets/logo.png"]`).
- `exclude` (list[string], optional): Glob patterns for files/directories to
exclude (e.g., `["**/*.log", "temp/"]`). These are added to default excludes
if `useDefaultExcludes` is true.
- `include` (list[string], optional): Additional glob patterns to include. These
are merged with `paths` (e.g., `["*.test.ts"]` to specifically add test files
if they were broadly excluded, or `["images/*.jpg"]` to include specific image
types).
- `recursive` (boolean, optional): Whether to search recursively. This is
primarily controlled by `**` in glob patterns. Defaults to `true`.
- `useDefaultExcludes` (boolean, optional): Whether to apply a list of default
exclusion patterns (e.g., `node_modules`, `.git`, non image/pdf binary files).
Defaults to `true`.
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore
patterns when finding files. Defaults to true.
## How to use `read_many_files` with the Gemini CLI
`read_many_files` searches for files matching the provided `paths` and `include`
patterns, while respecting `exclude` patterns and default excludes (if enabled).
- For text files: it reads the content of each matched file (attempting to skip
binary files not explicitly requested as image/PDF) and concatenates it into a
single string, with a separator `--- {filePath} ---` between the content of
each file. Uses UTF-8 encoding by default.
- The tool inserts a `--- End of content ---` after the last file.
- For image and PDF files: if explicitly requested by name or extension (e.g.,
`paths: ["logo.png"]` or `include: ["*.pdf"]`), the tool reads the file and
returns its content as a base64 encoded string.
- The tool attempts to detect and skip other binary files (those not matching
common image/PDF types or not explicitly requested) by checking for null bytes
in their initial content.
Usage:
```
read_many_files(paths=["Your files or paths here."], include=["Additional files to include."], exclude=["Files to exclude."], recursive=False, useDefaultExcludes=false, respect_git_ignore=true)
```
## `read_many_files` examples
Read all TypeScript files in the `src` directory:
```
read_many_files(paths=["src/**/*.ts"])
```
Read the main README, all Markdown files in the `docs` directory, and a specific
logo image, excluding a specific file:
```
read_many_files(paths=["README.md", "docs/**/*.md", "assets/logo.png"], exclude=["docs/OLD_README.md"])
```
Read all JavaScript files but explicitly include test files and all JPEGs in an
`images` folder:
```
read_many_files(paths=["**/*.js"], include=["**/*.test.js", "images/**/*.jpg"], useDefaultExcludes=False)
```
## Important notes
- **Binary file handling:**
- **Image/PDF/Audio/Video files:** The tool can read common image types (PNG,
JPEG, etc.), PDF, audio (mp3, wav), and video (mp4, mov) files, returning
them as base64 encoded data. These files _must_ be explicitly targeted by
the `paths` or `include` patterns (e.g., by specifying the exact filename
like `video.mp4` or a pattern like `*.mov`).
- **Other binary files:** The tool attempts to detect and skip other types of
binary files by examining their initial content for null bytes. The tool
excludes these files from its output.
- **Performance:** Reading a very large number of files or very large individual
files can be resource-intensive.
- **Path specificity:** Ensure paths and glob patterns are correctly specified
relative to the tool's target directory. For image/PDF files, ensure the
patterns are specific enough to include them.
- **Default excludes:** Be aware of the default exclusion patterns (like
`node_modules`, `.git`) and use `useDefaultExcludes=False` if you need to
override them, but do so cautiously.
+4 -3
View File
@@ -6,7 +6,8 @@ This document describes the `write_todos` tool for the Gemini CLI.
The `write_todos` tool allows the Gemini agent to create and manage a list of
subtasks for complex user requests. This provides you, the user, with greater
visibility into the agent's plan and its current progress.
visibility into the agent's plan and its current progress. It also helps with
alignment where the agent is less likely to lose track of its current goal.
### Arguments
@@ -49,8 +50,8 @@ write_todos({
## Important notes
- **Enabling:** This tool is disabled by default. To use it, you must enable it
in your `settings.json` file by setting `"useWriteTodos": true`.
- **Enabling:** This tool is enabled by default. You can disable it in your
`settings.json` file by setting `"useWriteTodos": false`.
- **Intended Use:** This tool is primarily used by the agent for complex,
multi-turn tasks. It is generally not used for simple, single-turn questions.
+25
View File
@@ -161,6 +161,31 @@ export default tseslint.config(
'default-case': 'error',
},
},
{
// Prevent self-imports in packages
files: ['packages/core/src/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
name: '@google/gemini-cli-core',
message: 'Please use relative imports within the @google/gemini-cli-core package.',
},
],
},
},
{
files: ['packages/cli/src/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
name: '@google/gemini-cli',
message: 'Please use relative imports within the @google/gemini-cli package.',
},
],
},
},
{
files: ['packages/*/src/**/*.test.{ts,tsx}'],
plugins: {
+33 -32
View File
@@ -4,49 +4,50 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { expect, test } from 'vitest';
import { describe, expect, it } from 'vitest';
import { TestRig } from './test-helper.js';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
const extension = `{
"name": "test-extension",
"name": "test-extension-install",
"version": "0.0.1"
}`;
const extensionUpdate = `{
"name": "test-extension",
"name": "test-extension-install",
"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 */
}
describe('extension install', () => {
it('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 {
const result = await rig.runCommand(
['extensions', 'install', `${rig.testDir!}`],
{ stdin: 'y\n' },
);
expect(result).toContain('test-extension-install');
const result = await rig.runCommand(
['extensions', 'install', `${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();
const listResult = await rig.runCommand(['extensions', 'list']);
expect(listResult).toContain('test-extension-install');
writeFileSync(testServerPath, extensionUpdate);
const updateResult = await rig.runCommand([
'extensions',
'update',
`test-extension-install`,
]);
expect(updateResult).toContain('0.0.2');
} finally {
await rig.runCommand([
'extensions',
'uninstall',
'test-extension-install',
]);
await rig.cleanup();
}
});
});
+119
View File
@@ -0,0 +1,119 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { expect, it, describe } from 'vitest';
import { TestRig } from './test-helper.js';
import { TestMcpServer } from './test-mcp-server.js';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { safeJsonStringify } from '@google/gemini-cli-core/src/utils/safeJsonStringify.js';
import { env } from 'node:process';
import { platform } from 'node:os';
const itIf = (condition: boolean) => (condition ? it : it.skip);
describe('extension reloading', () => {
const sandboxEnv = env['GEMINI_SANDBOX'];
// Fails in sandbox mode, can't check for local extension updates.
itIf((!sandboxEnv || sandboxEnv === 'false') && platform() !== 'win32')(
'installs a local extension, updates it, checks it was reloaded properly',
async () => {
const serverA = new TestMcpServer();
const portA = await serverA.start({
hello: () => ({ content: [{ type: 'text', text: 'world' }] }),
});
const extension = {
name: 'test-extension',
version: '0.0.1',
mcpServers: {
'test-server': {
httpUrl: `http://localhost:${portA}/mcp`,
},
},
};
const rig = new TestRig();
rig.setup('extension reload test', {
settings: {
experimental: { extensionReloading: true },
},
});
const testServerPath = join(rig.testDir!, 'gemini-extension.json');
writeFileSync(testServerPath, safeJsonStringify(extension, 2));
// defensive cleanup from previous tests.
try {
await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
} catch {
/* empty */
}
const result = await rig.runCommand(
['extensions', 'install', `${rig.testDir!}`],
{ stdin: 'y\n' },
);
expect(result).toContain('test-extension');
// Now create the update, but its not installed yet
const serverB = new TestMcpServer();
const portB = await serverB.start({
goodbye: () => ({ content: [{ type: 'text', text: 'world' }] }),
});
extension.version = '0.0.2';
extension.mcpServers['test-server'].httpUrl =
`http://localhost:${portB}/mcp`;
writeFileSync(testServerPath, safeJsonStringify(extension, 2));
// Start the CLI.
const run = await rig.runInteractive('--debug');
await run.expectText('You have 1 extension with an update available');
// See the outdated extension
await run.sendText('/extensions list');
await run.type('\r');
await run.expectText(
'test-extension (v0.0.1) - active (update available)',
);
await run.sendText('/mcp list');
await run.type('\r');
await run.expectText(
'test-server (from test-extension) - Ready (1 tool)',
);
await run.expectText('- hello');
// Update the extension, expect the list to update, and mcp servers as well.
await run.sendKeys('/extensions update test-extension');
await run.expectText('/extensions update test-extension');
await run.sendKeys('\r');
await new Promise((resolve) => setTimeout(resolve, 500));
await run.sendKeys('\r');
await run.expectText(
` * test-server (remote): http://localhost:${portB}/mcp`,
);
await run.type('\r'); // consent
await run.expectText(
'Extension "test-extension" successfully updated: 0.0.1 → 0.0.2',
);
await new Promise((resolve) => setTimeout(resolve, 1000));
await run.sendText('/extensions list');
await run.type('\r');
await run.expectText('test-extension (v0.0.2) - active (updated)');
await run.sendText('/mcp list');
await run.type('\r');
await run.expectText(
'test-server (from test-extension) - Ready (1 tool)',
);
await run.expectText('- goodbye');
await run.sendText('/quit');
await run.sendKeys('\r');
// Clean things up.
await serverA.stop();
await serverB.stop();
await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
await rig.cleanup();
},
);
});
+25 -14
View File
@@ -98,7 +98,9 @@ describe('file-system', () => {
await rig.setup('should correctly handle file paths with spaces');
const fileName = 'my test file.txt';
const result = await rig.run(`write "hello" to "${fileName}"`);
const result = await rig.run(
`write "hello" to "${fileName}" and then stop. Do not perform any other actions.`,
);
const foundToolCall = await rig.waitForToolCall('write_file');
if (!foundToolCall) {
@@ -150,44 +152,53 @@ describe('file-system', () => {
it.skip('should replace multiple instances of a string', async () => {
const rig = new TestRig();
await rig.setup('should replace multiple instances of a string');
rig.setup('should replace multiple instances of a string');
const fileName = 'ambiguous.txt';
const fileContent = 'Hey there, \ntest line\ntest line';
const expectedContent = 'Hey there, \nnew line\nnew line';
rig.createFile(fileName, fileContent);
const result = await rig.run(
`replace "test line" with "new line" in ${fileName}`,
`rewrite the file ${fileName} to replace all instances of "test line" with "new line"`,
);
const foundToolCall = await rig.waitForAnyToolCall([
'replace',
'write_file',
]);
const validTools = ['write_file', 'edit'];
const foundToolCall = await rig.waitForAnyToolCall(validTools);
if (!foundToolCall) {
printDebugInfo(rig, result);
printDebugInfo(rig, result, {
'Tool call found': foundToolCall,
'Tool logs': rig.readToolLogs(),
});
}
expect(
foundToolCall,
'Expected to find a replace or write_file tool call',
`Expected to find one of ${validTools.join(', ')} tool calls`,
).toBeTruthy();
const toolLogs = rig.readToolLogs();
const successfulEdit = toolLogs.some(
(log) =>
(log.toolRequest.name === 'replace' ||
log.toolRequest.name === 'write_file') &&
log.toolRequest.success,
validTools.includes(log.toolRequest.name) && log.toolRequest.success,
);
if (!successfulEdit) {
console.error(
'Expected a successful edit tool call, but none was found.',
`Expected a successful edit tool call (${validTools.join(', ')}), but none was found.`,
);
printDebugInfo(rig, result);
}
expect(successfulEdit, 'Expected a successful edit tool call').toBeTruthy();
expect(
successfulEdit,
`Expected a successful edit tool call (${validTools.join(', ')})`,
).toBeTruthy();
const newFileContent = rig.readFile(fileName);
if (newFileContent !== expectedContent) {
printDebugInfo(rig, result, {
'Final file content': newFileContent,
'Expected file content': expectedContent,
'Tool logs': rig.readToolLogs(),
});
}
expect(newFileContent).toBe(expectedContent);
});
File diff suppressed because one or more lines are too long
+23 -15
View File
@@ -6,26 +6,34 @@
import { describe, it, expect } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
describe('Flicker Detector', () => {
// TODO: https://github.com/google-gemini/gemini-cli/issues/11170
it.skip('should not detect a flicker under the max height budget', async () => {
it('should not detect a flicker under the max height budget', async () => {
const rig = new TestRig();
await rig.setup('flicker-detector-test');
rig.setup('flicker-detector-test', {
fakeResponsesPath: join(
import.meta.dirname,
'flicker-detector.max-height.responses',
),
});
try {
const run = await rig.runInteractive();
const prompt = 'Tell me a fun fact.';
await run.type(prompt);
await run.type('\r');
const run = await rig.runInteractive();
const prompt = 'Tell me a fun fact.';
await run.type(prompt);
await run.type('\r');
const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
expect(hasUserPromptEvent).toBe(true);
const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
expect(hasUserPromptEvent).toBe(true);
const hasSessionCountMetric = await rig.waitForMetric('session.count');
expect(hasSessionCountMetric).toBe(true);
const hasSessionCountMetric = await rig.waitForMetric('session.count');
expect(hasSessionCountMetric).toBe(true);
// We expect NO flicker event to be found.
const flickerMetric = rig.readMetric('ui.flicker.count');
expect(flickerMetric).toBeNull();
// We expect NO flicker event to be found.
const flickerMetric = rig.readMetric('ui.flicker.count');
expect(flickerMetric).toBeNull();
} finally {
await rig.cleanup();
}
});
});
+9
View File
@@ -12,6 +12,7 @@ if (process.env['NO_COLOR'] !== undefined) {
import { mkdir, readdir, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
@@ -32,6 +33,12 @@ export async function setup() {
// construct the path before the HOME env var is set.
process.env['GEMINI_CONFIG_DIR'] = join(runDir, '.gemini');
// Download ripgrep to avoid race conditions in parallel tests
const available = await canUseRipgrep();
if (!available) {
throw new Error('Failed to download ripgrep binary');
}
// Clean up old test runs, but keep the latest few for debugging
try {
const testRuns = await readdir(integrationTestsDir);
@@ -52,6 +59,8 @@ export async function setup() {
process.env['INTEGRATION_TEST_FILE_DIR'] = runDir;
process.env['GEMINI_CLI_INTEGRATION_TEST'] = 'true';
// Force file storage to avoid keychain prompts/hangs in CI, especially on macOS
process.env['GEMINI_FORCE_FILE_STORAGE'] = 'true';
process.env['TELEMETRY_LOG_FILE'] = join(runDir, 'telemetry.log');
if (process.env['KEEP_OUTPUT']) {
@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Investigating File Access**\n\nI'm currently focused on the challenge of reading a file. The path provided is `/gemini-cli/.integration-tests/1761766343238/json-output-error/path/to/nonexistent/file.txt`, and I'm anticipating an error. It's safe to assume the file doesn't exist, which I intend to handle by responding with \"File not found\" as instructed.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12303,"totalTokenCount":12418,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":115}},{"candidates":[{"content":{"parts":[{"thought":true,"text":"**Analyzing Error Handling**\n\nI've attempted to read the specified file, expecting an error due to the \"nonexistent\" path. My plan is to catch the error thrown by the `read_file` tool. Upon receiving this error, I'll promptly return \"File not found.\" This is in line with the initial instructions and ensures appropriate error management for the user's intended functionality. I'm now testing the error response.\n\n\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12303,"totalTokenCount":12467,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":164}},{"candidates":[{"content":{"parts":[{"thoughtSignature":"CiQB0e2Kb0c450IIdZRHl1vvjWDAl9oKa7s5sfFgrTnU0w3qBQwKYgHR7YpvPjZlXSHaJNYgX8IuCvxfyyeACX4NZ8u+u3Z1kqQbgOMpiH6aPYCK9lPyhVftPDBF2m3j7Y2AShwXSpB+9+UB3zphOKCvq6i0ZLvK6QzVynZ1fySQacyjEBD+U6y5CpoBAdHtim9D6oskRu7f3x8rp56h24i6dwb1hzlyqLGl3A5Hsh/fGYjBCxR+Vs+U5Sb7LunmFMKxmO1fktz0x06FUiyaWgAaXl4E4FyLTKs9BbPdgo58uAhaI3vTQybeGZQzkRi6n+ywCniVKaTUd07EHYWdvLiB6x5zFKVOiTmh8PC6I4vNOkJIzFXRfR5aM5QFjEQY8R88HBH+ugraAQHR7YpvuNUG8Ttbzg+kq+kJYMNGDG4zVLPxFBbJZYU5zGLFLrNeEpp9HJ+1N1Pdts17J5pzoefKd2U9H5muPxoeW1CCpFqqKfYZNyoChrQnb4CgWoJ92pcb2UAZbDQ6H4rTyXWdXaAEP1i8rOmdWzI5GFPyxCwBKAbFhfMJOo7P4va5H13XTlLgWAgXlx3n24ookkD/DOL9ro/F6pmV6tzf/j7RVl0V5yozngnrFl4dFsQPp1nDQcFkbRIKHD30byjLGh6Vd6A2lrB477qUaCRfPXtlEgKgsOSYCuABAdHtim+PJPZV3QuQoJ0Qokr+vcdW1W2qzC4Vvt4QRJXyp6+eIS6iNjOisk4jfWLHmLTa11KrUSQsFJnGwsFhe1foOqfO4y46ROBPeSdyuc0gDzSCZPowiovGBwoZd+MbPTTVyExWBo6StVk+xWLj+sUqa8VDof45AOOK9kdzcLQPa2SXIeK6SYLS1zVTrbzGHQDZPv9UC99lmKxPFqAcuwpe9CQre9J3GcZSwd5Mfs/UkHbWui10a8jga4Ck7umjKSDFq/VWiRUp/jh+EN4xeLTG/UQhgH49Gw+Bi1hN1uYKIgHR7YpvS0j1oO2HV/HeK3YxSku9CLQ3OSJGhoAKiu8UGYE=","functionCall":{"name":"read_file","args":{"file_path":"/gemini-cli/.integration-tests/1761766343238/json-output-error/path/to/nonexistent/file.txt"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12303,"candidatesTokenCount":58,"totalTokenCount":12525,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12303}],"thoughtsTokenCount":164}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"File not found"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12588,"candidatesTokenCount":3,"totalTokenCount":12591,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12588}]}}]}
+8 -1
View File
@@ -6,6 +6,7 @@
import { expect, describe, it, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
describe('JSON output', () => {
let rig: TestRig;
@@ -87,7 +88,13 @@ describe('JSON output', () => {
expect(payload.error.message).toContain("current type is 'oauth-personal'");
});
it.skip('should not exit on tool errors and allow model to self-correct in JSON mode', async () => {
it('should not exit on tool errors and allow model to self-correct in JSON mode', async () => {
rig.setup('json-output-error', {
fakeResponsesPath: join(
import.meta.dirname,
'json-output.error.responses',
),
});
const result = await rig.run(
`Read the contents of ${rig.testDir}/path/to/nonexistent/file.txt and tell me what it says. ` +
'On error, respond to the user with exactly the text "File not found".',
+20 -26
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { describe, it } from 'vitest';
import {
TestRig,
poll,
@@ -17,7 +17,7 @@ import { join } from 'node:path';
describe('list_directory', () => {
it('should be able to list a directory', async () => {
const rig = new TestRig();
await rig.setup('should be able to list a directory');
rig.setup('should be able to list a directory');
rig.createFile('file1.txt', 'file 1 content');
rig.mkdir('subdir');
rig.sync();
@@ -38,33 +38,27 @@ describe('list_directory', () => {
const result = await rig.run(prompt);
const foundToolCall = await rig.waitForToolCall('list_directory');
try {
await rig.expectToolCallSuccess(['list_directory']);
} catch (e) {
// Add debugging information
if (!result.includes('file1.txt') || !result.includes('subdir')) {
const allTools = printDebugInfo(rig, result, {
'Found tool call': false,
'Contains file1.txt': result.includes('file1.txt'),
'Contains subdir': result.includes('subdir'),
});
// Add debugging information
if (
!foundToolCall ||
!result.includes('file1.txt') ||
!result.includes('subdir')
) {
const allTools = printDebugInfo(rig, result, {
'Found tool call': foundToolCall,
'Contains file1.txt': result.includes('file1.txt'),
'Contains subdir': result.includes('subdir'),
});
console.error(
'List directory calls:',
allTools
.filter((t) => t.toolRequest.name === 'list_directory')
.map((t) => t.toolRequest.args),
);
console.error(
'List directory calls:',
allTools
.filter((t) => t.toolRequest.name === 'list_directory')
.map((t) => t.toolRequest.args),
);
}
throw e;
}
expect(
foundToolCall,
'Expected to find a list_directory tool call',
).toBeTruthy();
// Validate model output - will throw if no output, warn if missing expected content
validateModelOutput(result, ['file1.txt', 'subdir'], 'List directory test');
});
@@ -43,5 +43,6 @@ describe('read_many_files', () => {
// Validate model output - will throw if no output
validateModelOutput(result, null, 'Read many files test');
await rig.cleanup();
});
});
+91
View File
@@ -0,0 +1,91 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import { RipGrepTool } from '../packages/core/src/tools/ripGrep.js';
import { Config } from '../packages/core/src/config/config.js';
import { WorkspaceContext } from '../packages/core/src/utils/workspaceContext.js';
// Mock Config to provide necessary context
class MockConfig {
constructor(private targetDir: string) {}
getTargetDir() {
return this.targetDir;
}
getWorkspaceContext() {
return new WorkspaceContext(this.targetDir, [this.targetDir]);
}
getDebugMode() {
return true;
}
}
describe('ripgrep-real-direct', () => {
let tempDir: string;
let tool: RipGrepTool;
beforeAll(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ripgrep-real-test-'));
// Create test files
await fs.writeFile(path.join(tempDir, 'file1.txt'), 'hello world\n');
await fs.mkdir(path.join(tempDir, 'subdir'));
await fs.writeFile(
path.join(tempDir, 'subdir', 'file2.txt'),
'hello universe\n',
);
await fs.writeFile(path.join(tempDir, 'file3.txt'), 'goodbye moon\n');
const config = new MockConfig(tempDir) as unknown as Config;
tool = new RipGrepTool(config);
});
afterAll(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('should find matches using the real ripgrep binary', async () => {
const invocation = tool.build({ pattern: 'hello' });
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toContain('Found 2 matches');
expect(result.llmContent).toContain('file1.txt');
expect(result.llmContent).toContain('L1: hello world');
expect(result.llmContent).toContain('subdir'); // Should show path
expect(result.llmContent).toContain('file2.txt');
expect(result.llmContent).toContain('L1: hello universe');
expect(result.llmContent).not.toContain('goodbye moon');
});
it('should handle no matches correctly', async () => {
const invocation = tool.build({ pattern: 'nonexistent_pattern_123' });
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toContain('No matches found');
});
it('should respect include filters', async () => {
// Create a .js file
await fs.writeFile(
path.join(tempDir, 'script.js'),
'console.log("hello");\n',
);
const invocation = tool.build({ pattern: 'hello', include: '*.js' });
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toContain('Found 1 match');
expect(result.llmContent).toContain('script.js');
expect(result.llmContent).not.toContain('file1.txt');
});
});
+1 -1
View File
@@ -191,7 +191,7 @@ describe('run_shell_command', () => {
expect(toolCall.toolRequest.success).toBe(true);
});
it('should succeed with no parens in non-interactive mode', async () => {
it.skip('should succeed with no parens in non-interactive mode', async () => {
const rig = new TestRig();
await rig.setup('should succeed with no parens in non-interactive mode');
+20 -5
View File
@@ -192,7 +192,12 @@ export class InteractiveRun {
timeout,
200,
);
expect(found, `Did not find expected text: "${text}"`).toBe(true);
expect(
found,
`Did not find expected text: "${text}". Output was:\n${stripAnsi(
this.output,
)}`,
).toBe(true);
}
// This types slowly to make sure command is correct, but only work for short
@@ -220,6 +225,13 @@ export class InteractiveRun {
}
}
// Types an entire string at once, necessary for some things like commands
// but may run into paste detection issues for larger strings.
async sendText(text: string) {
this.ptyProcess.write(text);
await new Promise((resolve) => setTimeout(resolve, 5));
}
// Simulates typing a string one character at a time to avoid paste detection.
async sendKeys(text: string) {
const delay = 5;
@@ -296,6 +308,7 @@ export class TestRig {
// Nightly releases sometimes becomes out of sync with local code and
// triggers auto-update, which causes tests to fail.
disableAutoUpdate: true,
previewFeatures: false,
},
telemetry: {
enabled: true,
@@ -311,6 +324,8 @@ export class TestRig {
model: DEFAULT_GEMINI_MODEL,
sandbox:
env['GEMINI_SANDBOX'] !== 'false' ? env['GEMINI_SANDBOX'] : false,
// Don't show the IDE connection dialog when running from VsCode
ide: { enabled: false, hasSeenNudge: true },
...options.settings, // Allow tests to override/add settings
};
writeFileSync(
@@ -386,13 +401,13 @@ export class TestRig {
};
if (typeof promptOrOptions === 'string') {
commandArgs.push('--prompt', promptOrOptions);
commandArgs.push(promptOrOptions);
} else if (
typeof promptOrOptions === 'object' &&
promptOrOptions !== null
) {
if (promptOrOptions.prompt) {
commandArgs.push('--prompt', promptOrOptions.prompt);
commandArgs.push(promptOrOptions.prompt);
}
if (promptOrOptions.stdin) {
execOptions.input = promptOrOptions.stdin;
@@ -995,7 +1010,7 @@ export class TestRig {
const options: pty.IPtyForkOptions = {
name: 'xterm-color',
cols: 80,
rows: 24,
rows: 80,
cwd: this.testDir!,
env: Object.fromEntries(
Object.entries(env).filter(([, v]) => v !== undefined),
@@ -1007,7 +1022,7 @@ export class TestRig {
const run = new InteractiveRun(ptyProcess);
// Wait for the app to be ready
await run.expectText('Type your message', 30000);
await run.expectText(' Type your message or @path/to/file', 30000);
return run;
}
}
+26 -10
View File
@@ -4,17 +4,21 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
McpServer,
type ToolCallback,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { type Server as HTTPServer } from 'node:http';
import { randomUUID } from 'node:crypto';
import { type ZodRawShape } from 'zod';
export class TestMcpServer {
private server: HTTPServer | undefined;
async start(): Promise<number> {
async start(
tools?: Record<string, ToolCallback<ZodRawShape>>,
): Promise<number> {
const app = express();
app.use(express.json());
const mcpServer = new McpServer(
@@ -22,18 +26,30 @@ export class TestMcpServer {
name: 'test-mcp-server',
version: '1.0.0',
},
{ capabilities: {} },
{ capabilities: { tools: {} } },
);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
mcpServer.connect(transport);
if (tools) {
for (const [name, cb] of Object.entries(tools)) {
mcpServer.registerTool(name, {}, cb);
}
}
app.post('/mcp', async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
res.on('close', () => {
transport.close();
});
await mcpServer.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.get('/mcp', async (req, res) => {
res.status(405).send('Not supported');
});
return new Promise((resolve, reject) => {
this.server = app.listen(0, () => {
const address = this.server!.address();
+1010 -834
View File
File diff suppressed because it is too large Load Diff
+17 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.19.0-nightly.20251121.5982abeff",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.19.0-nightly.20251121.5982abeff"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -27,6 +27,10 @@
"auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev",
"auth": "npm run auth:npm && npm run auth:docker",
"generate": "node scripts/generate-git-commit-info.js",
"predocs:settings": "npm run build --workspace @google/gemini-cli-core",
"schema:settings": "tsx ./scripts/generate-settings-schema.ts",
"docs:settings": "tsx ./scripts/generate-settings-doc.ts",
"docs:keybindings": "tsx ./scripts/generate-keybindings-doc.ts",
"build": "node scripts/build.js",
"build-and-start": "npm run build && npm run start",
"build:vscode": "node scripts/build_vscode_companion.js",
@@ -58,6 +62,7 @@
"pre-commit": "node scripts/pre-commit.js"
},
"overrides": {
"ink": "npm:@jrichman/ink@6.4.5",
"wrap-ansi": "9.0.2",
"cliui": {
"wrap-ansi": "7.0.0"
@@ -78,6 +83,8 @@
"@types/minimatch": "^5.1.2",
"@types/mock-fs": "^4.13.4",
"@types/prompts": "^2.4.9",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/shell-quote": "^1.7.5",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/eslint-plugin": "^1.3.4",
@@ -90,10 +97,11 @@
"eslint-plugin-license-header": "^0.8.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"glob": "^10.4.5",
"glob": "^12.0.0",
"globals": "^16.0.0",
"google-artifactregistry-auth": "^3.4.0",
"husky": "^9.1.7",
"ink-testing-library": "^4.0.0",
"json": "^11.0.0",
"lint-staged": "^16.1.6",
"memfs": "^4.42.0",
@@ -102,7 +110,7 @@
"msw": "^2.10.4",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"react-devtools-core": "^4.28.5",
"react-devtools-core": "^6.1.2",
"semver": "^7.7.2",
"strip-ansi": "^7.1.2",
"tsx": "^4.20.3",
@@ -111,7 +119,7 @@
"yargs": "^17.7.2"
},
"dependencies": {
"@testing-library/dom": "^10.4.1",
"ink": "npm:@jrichman/ink@6.4.5",
"latest-version": "^9.0.0",
"simple-git": "^3.28.0"
},
@@ -127,7 +135,10 @@
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix --max-warnings 0"
"eslint --fix --max-warnings 0 --no-warn-ignored"
],
"eslint.config.js": [
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.19.0-nightly.20251121.5982abeff",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"tar": "^7.4.3",
"tar": "^7.5.2",
"uuid": "^11.1.0",
"winston": "^3.17.0"
},
+1 -2
View File
@@ -113,7 +113,7 @@ export class Task {
// state managed within the @gemini-cli/core module.
async getMetadata(): Promise<TaskMetadata> {
const toolRegistry = await this.config.getToolRegistry();
const mcpServers = this.config.getMcpServers() || {};
const mcpServers = this.config.getMcpClientManager()?.getMcpServers() || {};
const serverStatuses = getAllMCPServerStatuses();
const servers = Object.keys(mcpServers).map((serverName) => ({
name: serverName,
@@ -436,7 +436,6 @@ export class Task {
onToolCallsUpdate: this._schedulerToolCallsUpdate.bind(this),
getPreferredEditor: () => 'vscode',
config: this.config,
onEditorClose: () => {},
});
return scheduler;
}
@@ -5,27 +5,44 @@
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Command } from './types.js';
describe('CommandRegistry', () => {
const mockListExtensionsCommandInstance = {
names: ['extensions', 'extensions list'],
const mockListExtensionsCommandInstance: Command = {
name: 'extensions list',
description: 'Lists all installed extensions.',
execute: vi.fn(),
};
const mockListExtensionsCommand = vi.fn(
() => mockListExtensionsCommandInstance,
);
const mockExtensionsCommandInstance: Command = {
name: 'extensions',
description: 'Manage extensions.',
execute: vi.fn(),
subCommands: [mockListExtensionsCommandInstance],
};
const mockExtensionsCommand = vi.fn(() => mockExtensionsCommandInstance);
beforeEach(async () => {
vi.resetModules();
vi.doMock('./list-extensions', () => ({
vi.doMock('./extensions.js', () => ({
ExtensionsCommand: mockExtensionsCommand,
ListExtensionsCommand: mockListExtensionsCommand,
}));
});
it('should register ListExtensionsCommand on initialization', async () => {
it('should register ExtensionsCommand on initialization', async () => {
const { commandRegistry } = await import('./command-registry.js');
expect(mockListExtensionsCommand).toHaveBeenCalled();
expect(mockExtensionsCommand).toHaveBeenCalled();
const command = commandRegistry.get('extensions');
expect(command).toBe(mockExtensionsCommandInstance);
});
it('should register sub commands on initialization', async () => {
const { commandRegistry } = await import('./command-registry.js');
const command = commandRegistry.get('extensions list');
expect(command).toBe(mockListExtensionsCommandInstance);
});
@@ -37,12 +54,65 @@ describe('CommandRegistry', () => {
it('register() should register a new command', async () => {
const { commandRegistry } = await import('./command-registry.js');
const mockCommand = {
names: ['test-command'],
const mockCommand: Command = {
name: 'test-command',
description: '',
execute: vi.fn(),
};
commandRegistry.register(mockCommand);
const command = commandRegistry.get('test-command');
expect(command).toBe(mockCommand);
});
it('register() should register a nested command', async () => {
const { commandRegistry } = await import('./command-registry.js');
const mockSubSubCommand: Command = {
name: 'test-command-sub-sub',
description: '',
execute: vi.fn(),
};
const mockSubCommand: Command = {
name: 'test-command-sub',
description: '',
execute: vi.fn(),
subCommands: [mockSubSubCommand],
};
const mockCommand: Command = {
name: 'test-command',
description: '',
execute: vi.fn(),
subCommands: [mockSubCommand],
};
commandRegistry.register(mockCommand);
const command = commandRegistry.get('test-command');
const subCommand = commandRegistry.get('test-command-sub');
const subSubCommand = commandRegistry.get('test-command-sub-sub');
expect(command).toBe(mockCommand);
expect(subCommand).toBe(mockSubCommand);
expect(subSubCommand).toBe(mockSubSubCommand);
});
it('register() should not enter an infinite loop with a cyclic command', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const { commandRegistry } = await import('./command-registry.js');
const mockCommand: Command = {
name: 'cyclic-command',
description: '',
subCommands: [],
execute: vi.fn(),
};
mockCommand.subCommands?.push(mockCommand); // Create cycle
commandRegistry.register(mockCommand);
expect(commandRegistry.get('cyclic-command')).toBe(mockCommand);
expect(warnSpy).toHaveBeenCalledWith(
'Command cyclic-command already registered. Skipping.',
);
// If the test finishes, it means we didn't get into an infinite loop.
warnSpy.mockRestore();
});
});
@@ -4,30 +4,36 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { ListExtensionsCommand } from './list-extensions.js';
import type { Config } from '@google/gemini-cli-core';
export interface Command {
readonly names: string[];
execute(config: Config, args: string[]): Promise<unknown>;
}
import { ExtensionsCommand } from './extensions.js';
import type { Command } from './types.js';
class CommandRegistry {
private readonly commands = new Map<string, Command>();
constructor() {
this.register(new ListExtensionsCommand());
this.register(new ExtensionsCommand());
}
register(command: Command) {
for (const name of command.names) {
this.commands.set(name, command);
if (this.commands.has(command.name)) {
console.warn(`Command ${command.name} already registered. Skipping.`);
return;
}
this.commands.set(command.name, command);
for (const subCommand of command.subCommands ?? []) {
this.register(subCommand);
}
}
get(commandName: string): Command | undefined {
return this.commands.get(commandName);
}
getAllCommands(): Command[] {
return [...this.commands.values()];
}
}
export const commandRegistry = new CommandRegistry();
@@ -0,0 +1,87 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { ExtensionsCommand, ListExtensionsCommand } from './extensions.js';
import type { Config } from '@google/gemini-cli-core';
const mockListExtensions = vi.hoisted(() => vi.fn());
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...original,
listExtensions: mockListExtensions,
};
});
describe('ExtensionsCommand', () => {
it('should have the correct name', () => {
const command = new ExtensionsCommand();
expect(command.name).toEqual('extensions');
});
it('should have the correct description', () => {
const command = new ExtensionsCommand();
expect(command.description).toEqual('Manage extensions.');
});
it('should have "extensions list" as a subcommand', () => {
const command = new ExtensionsCommand();
expect(command.subCommands.map((c) => c.name)).toContain('extensions list');
});
it('should be a top-level command', () => {
const command = new ExtensionsCommand();
expect(command.topLevel).toBe(true);
});
it('should default to listing extensions', async () => {
const command = new ExtensionsCommand();
const mockConfig = {} as Config;
const mockExtensions = [{ name: 'ext1' }];
mockListExtensions.mockReturnValue(mockExtensions);
const result = await command.execute(mockConfig, []);
expect(result).toEqual({ name: 'extensions list', data: mockExtensions });
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig);
});
});
describe('ListExtensionsCommand', () => {
it('should have the correct name', () => {
const command = new ListExtensionsCommand();
expect(command.name).toEqual('extensions list');
});
it('should call listExtensions with the provided config', async () => {
const command = new ListExtensionsCommand();
const mockConfig = {} as Config;
const mockExtensions = [{ name: 'ext1' }];
mockListExtensions.mockReturnValue(mockExtensions);
const result = await command.execute(mockConfig, []);
expect(result).toEqual({ name: 'extensions list', data: mockExtensions });
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig);
});
it('should return a message when no extensions are installed', async () => {
const command = new ListExtensionsCommand();
const mockConfig = {} as Config;
mockListExtensions.mockReturnValue([]);
const result = await command.execute(mockConfig, []);
expect(result).toEqual({
name: 'extensions list',
data: 'No extensions installed.',
});
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig);
});
});
@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { listExtensions, type Config } from '@google/gemini-cli-core';
import type { Command, CommandExecutionResponse } from './types.js';
export class ExtensionsCommand implements Command {
readonly name = 'extensions';
readonly description = 'Manage extensions.';
readonly subCommands = [new ListExtensionsCommand()];
readonly topLevel = true;
async execute(
config: Config,
_: string[],
): Promise<CommandExecutionResponse> {
return new ListExtensionsCommand().execute(config, _);
}
}
export class ListExtensionsCommand implements Command {
readonly name = 'extensions list';
readonly description = 'Lists all installed extensions.';
async execute(
config: Config,
_: string[],
): Promise<CommandExecutionResponse> {
const extensions = listExtensions(config);
const data = extensions.length ? extensions : 'No extensions installed.';
return { name: this.name, data };
}
}
@@ -1,39 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { ListExtensionsCommand } from './list-extensions.js';
import type { Config } from '@google/gemini-cli-core';
const mockListExtensions = vi.hoisted(() => vi.fn());
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...original,
listExtensions: mockListExtensions,
};
});
describe('ListExtensionsCommand', () => {
it('should have the correct names', () => {
const command = new ListExtensionsCommand();
expect(command.names).toEqual(['extensions', 'extensions list']);
});
it('should call listExtensions with the provided config', async () => {
const command = new ListExtensionsCommand();
const mockConfig = {} as Config;
const mockExtensions = [{ name: 'ext1' }];
mockListExtensions.mockReturnValue(mockExtensions);
const result = await command.execute(mockConfig, []);
expect(result).toEqual(mockExtensions);
expect(mockListExtensions).toHaveBeenCalledWith(mockConfig);
});
});
@@ -1,16 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { listExtensions, type Config } from '@google/gemini-cli-core';
import type { Command } from './command-registry.js';
export class ListExtensionsCommand implements Command {
readonly names = ['extensions', 'extensions list'];
async execute(config: Config, _: string[]): Promise<unknown> {
return listExtensions(config);
}
}
+28
View File
@@ -0,0 +1,28 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '@google/gemini-cli-core';
export interface CommandArgument {
readonly name: string;
readonly description: string;
readonly isRequired?: boolean;
}
export interface Command {
readonly name: string;
readonly description: string;
readonly arguments?: CommandArgument[];
readonly subCommands?: Command[];
readonly topLevel?: boolean;
execute(config: Config, args: string[]): Promise<CommandExecutionResponse>;
}
export interface CommandExecutionResponse {
readonly name: string;
readonly data: unknown;
}
+1 -23
View File
@@ -20,9 +20,7 @@ import {
GEMINI_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_GEMINI_MODEL,
type GeminiCLIExtension,
type ExtensionLoader,
debugLogger,
} from '@google/gemini-cli-core';
import { logger } from '../utils/logger.js';
@@ -34,7 +32,6 @@ export async function loadConfig(
extensionLoader: ExtensionLoader,
taskId: string,
): Promise<Config> {
const mcpServers = mergeMcpServers(settings, extensionLoader.getExtensions());
const workspaceDir = process.cwd();
const adcFilePath = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
@@ -54,7 +51,7 @@ export async function loadConfig(
process.env['GEMINI_YOLO_MODE'] === 'true'
? ApprovalMode.YOLO
: ApprovalMode.DEFAULT,
mcpServers,
mcpServers: settings.mcpServers,
cwd: workspaceDir,
telemetry: {
enabled: settings.telemetry?.enabled,
@@ -120,25 +117,6 @@ export async function loadConfig(
return config;
}
export function mergeMcpServers(
settings: Settings,
extensions: GeminiCLIExtension[],
) {
const mcpServers = { ...(settings.mcpServers || {}) };
for (const extension of extensions) {
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
if (mcpServers[key]) {
debugLogger.warn(
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
);
return;
}
mcpServers[key] = server;
});
}
return mcpServers;
}
export function setTargetDir(agentSettings: AgentSettings | undefined): string {
const originalCWD = process.cwd();
const targetDir =
+104 -1
View File
@@ -28,6 +28,7 @@ import {
vi,
} from 'vitest';
import { createApp } from './app.js';
import { commandRegistry } from '../commands/command-registry.js';
import {
assertUniqueFinalEventIsLast,
assertTaskCreationAndWorkingStatus,
@@ -35,6 +36,7 @@ import {
createMockConfig,
} from '../utils/testing_utils.js';
import { MockTool } from '@google/gemini-cli-core';
import type { Command } from '../commands/types.js';
const mockToolConfirmationFn = async () =>
({}) as unknown as ToolCallConfirmationDetails;
@@ -827,6 +829,104 @@ describe('E2E Tests', () => {
expect(thoughtEvent.metadata?.['traceId']).toBe(traceId);
});
describe('/listCommands', () => {
it('should return a list of top-level commands', async () => {
const mockCommands = [
{
name: 'test-command',
description: 'A test command',
topLevel: true,
arguments: [{ name: 'arg1', description: 'Argument 1' }],
subCommands: [
{
name: 'sub-command',
description: 'A sub command',
topLevel: false,
execute: vi.fn(),
},
],
execute: vi.fn(),
},
{
name: 'another-command',
description: 'Another test command',
topLevel: true,
execute: vi.fn(),
},
{
name: 'not-top-level',
description: 'Not a top level command',
topLevel: false,
execute: vi.fn(),
},
];
const getAllCommandsSpy = vi
.spyOn(commandRegistry, 'getAllCommands')
.mockReturnValue(mockCommands);
const agent = request.agent(app);
const res = await agent.get('/listCommands').expect(200);
expect(res.body).toEqual({
commands: [
{
name: 'test-command',
description: 'A test command',
arguments: [{ name: 'arg1', description: 'Argument 1' }],
subCommands: [
{
name: 'sub-command',
description: 'A sub command',
arguments: [],
subCommands: [],
},
],
},
{
name: 'another-command',
description: 'Another test command',
arguments: [],
subCommands: [],
},
],
});
expect(getAllCommandsSpy).toHaveBeenCalledOnce();
getAllCommandsSpy.mockRestore();
});
it('should handle cyclic commands gracefully', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const cyclicCommand: Command = {
name: 'cyclic-command',
description: 'A cyclic command',
topLevel: true,
execute: vi.fn(),
subCommands: [],
};
cyclicCommand.subCommands?.push(cyclicCommand); // Create cycle
const getAllCommandsSpy = vi
.spyOn(commandRegistry, 'getAllCommands')
.mockReturnValue([cyclicCommand]);
const agent = request.agent(app);
const res = await agent.get('/listCommands').expect(200);
expect(res.body.commands[0].name).toBe('cyclic-command');
expect(res.body.commands[0].subCommands).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
'Command cyclic-command already inserted in the response, skipping',
);
getAllCommandsSpy.mockRestore();
warnSpy.mockRestore();
});
});
describe('/executeCommand', () => {
const mockExtensions = [{ name: 'test-extension', version: '0.0.1' }];
@@ -846,7 +946,10 @@ describe('E2E Tests', () => {
.set('Content-Type', 'application/json')
.expect(200);
expect(res.body).toEqual(mockExtensions);
expect(res.body).toEqual({
name: 'extensions list',
data: mockExtensions,
});
expect(getExtensionsSpy).toHaveBeenCalled();
});
+50
View File
@@ -21,6 +21,14 @@ import { loadSettings } from '../config/settings.js';
import { loadExtensions } from '../config/extension.js';
import { commandRegistry } from '../commands/command-registry.js';
import { SimpleExtensionLoader } from '@google/gemini-cli-core';
import type { Command, CommandArgument } from '../commands/types.js';
type CommandResponse = {
name: string;
description: string;
arguments: CommandArgument[];
subCommands: CommandResponse[];
};
const coderAgentCard: AgentCard = {
name: 'Gemini SDLC Agent',
@@ -167,6 +175,48 @@ export async function createApp() {
}
});
expressApp.get('/listCommands', (req, res) => {
try {
const transformCommand = (
command: Command,
visited: string[],
): CommandResponse | undefined => {
const commandName = command.name;
if (visited.includes(commandName)) {
console.warn(
`Command ${commandName} already inserted in the response, skipping`,
);
return undefined;
}
return {
name: command.name,
description: command.description,
arguments: command.arguments ?? [],
subCommands: (command.subCommands ?? [])
.map((subCommand) =>
transformCommand(subCommand, visited.concat(commandName)),
)
.filter(
(subCommand): subCommand is CommandResponse => !!subCommand,
),
};
};
const commands = commandRegistry
.getAllCommands()
.filter((command) => command.topLevel)
.map((command) => transformCommand(command, []));
return res.status(200).json({ commands });
} catch (e) {
logger.error('Error executing /listCommands:', e);
const errorMessage =
e instanceof Error ? e.message : 'Unknown error listing commands';
return res.status(500).json({ 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)) {
@@ -53,6 +53,7 @@ export function createMockConfig(
getEnableMessageBusIntegration: vi.fn().mockReturnValue(false),
getMessageBus: vi.fn(),
getPolicyEngine: vi.fn(),
getEnableExtensionReloading: vi.fn().mockReturnValue(false),
...overrides,
} as unknown as Config;
-1
View File
@@ -11,7 +11,6 @@ export default defineConfig({
test: {
include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
exclude: ['**/node_modules/**', '**/dist/**'],
environment: 'jsdom',
globals: true,
reporters: ['default', 'junit'],
silent: true,
@@ -0,0 +1,165 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useEffect, useRef } from 'react';
import { render, Box, Text, useInput, useStdout } from 'ink';
import {
ScrollableList,
type ScrollableListRef,
} from '../src/ui/components/shared/ScrollableList.js';
import { ScrollProvider } from '../src/ui/contexts/ScrollProvider.js';
import { MouseProvider } from '../src/ui/contexts/MouseContext.js';
import { KeypressProvider } from '../src/ui/contexts/KeypressContext.js';
import {
enableMouseEvents,
disableMouseEvents,
} from '../src/ui/utils/mouse.js';
interface Item {
id: string;
title: string;
}
const getLorem = (index: number) =>
Array(10)
.fill(null)
.map(() => 'lorem ipsum '.repeat((index % 3) + 1).trim())
.join('\n');
const Demo = () => {
const { stdout } = useStdout();
const [size, setSize] = useState({
columns: stdout.columns,
rows: stdout.rows,
});
useEffect(() => {
const onResize = () => {
setSize({
columns: stdout.columns,
rows: stdout.rows,
});
};
stdout.on('resize', onResize);
return () => {
stdout.off('resize', onResize);
};
}, [stdout]);
const [items, setItems] = useState<Item[]>(() =>
Array.from({ length: 1000 }, (_, i) => ({
id: String(i),
title: `Item ${i + 1}`,
})),
);
const listRef = useRef<ScrollableListRef<Item>>(null);
useInput((input, key) => {
if (input === 'a' || input === 'A') {
setItems((prev) => [
...prev,
{ id: String(prev.length), title: `Item ${prev.length + 1}` },
]);
}
if ((input === 'e' || input === 'E') && !key.ctrl) {
setItems((prev) => {
if (prev.length === 0) return prev;
const lastIndex = prev.length - 1;
const lastItem = prev[lastIndex]!;
const newItem = { ...lastItem, title: lastItem.title + 'e' };
return [...prev.slice(0, lastIndex), newItem];
});
}
if (key.ctrl && input === 'e') {
listRef.current?.scrollToEnd();
}
// Let Ink handle Ctrl+C via exitOnCtrlC (default true) or handle explicitly if needed.
// For alternate buffer, explicit handling is often safer for cleanup.
if (key.escape || (key.ctrl && input === 'c')) {
process.exit(0);
}
});
return (
<MouseProvider mouseEventsEnabled={true}>
<KeypressProvider>
<ScrollProvider>
<Box
flexDirection="column"
width={size.columns}
height={size.rows - 1}
padding={1}
>
<Text>
Press &apos;A&apos; to add an item. Press &apos;E&apos; to edit
last item. Press &apos;Ctrl+E&apos; to scroll to end. Press
&apos;Esc&apos; to exit. Mouse wheel or Shift+Up/Down to scroll.
</Text>
<Box flexGrow={1} borderStyle="round" borderColor="cyan">
<ScrollableList
ref={listRef}
data={items}
renderItem={({ item, index }) => (
<Box flexDirection="column" paddingBottom={2}>
<Box
sticky
flexDirection="column"
width={size.columns - 2}
opaque
stickyChildren={
<Box
flexDirection="column"
width={size.columns - 2}
opaque
>
<Text>{item.title}</Text>
<Box
borderStyle="single"
borderTop={true}
borderBottom={false}
borderLeft={false}
borderRight={false}
borderColor="gray"
/>
</Box>
}
>
<Text>{item.title}</Text>
</Box>
<Text color="gray">{getLorem(index)}</Text>
</Box>
)}
estimatedItemHeight={() => 14}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
initialScrollOffsetInIndex={Number.MAX_SAFE_INTEGER}
/>
</Box>
<Text>Count: {items.length}</Text>
</Box>
</ScrollProvider>
</KeypressProvider>
</MouseProvider>
);
};
// Enable mouse reporting before rendering
enableMouseEvents();
// Ensure cleanup happens on exit
process.on('exit', () => {
disableMouseEvents();
});
// Handle SIGINT explicitly to ensure cleanup runs if Ink doesn't catch it in time
process.on('SIGINT', () => {
process.exit(0);
});
render(<Demo />, { alternateBuffer: true });
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.19.0-nightly.20251121.5982abeff",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,24 +25,25 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.19.0-nightly.20251121.5982abeff"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.16.0",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.15.1",
"@modelcontextprotocol/sdk": "^1.22.0",
"@types/update-notifier": "^6.0.8",
"ansi-regex": "^6.2.2",
"clipboardy": "^5.0.0",
"command-exists": "^1.2.9",
"comment-json": "^4.2.5",
"diff": "^7.0.0",
"dotenv": "^17.1.0",
"extract-zip": "^2.0.1",
"fzf": "^0.5.2",
"glob": "^10.4.5",
"glob": "^12.0.0",
"highlight.js": "^11.11.1",
"ink": "^6.2.3",
"ink": "npm:@jrichman/ink@6.4.5",
"ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0",
"latest-version": "^9.0.0",
@@ -50,14 +51,15 @@
"mnemonist": "^0.40.3",
"open": "^10.1.2",
"prompts": "^2.4.2",
"react": "^19.1.0",
"react": "^19.2.0",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.3",
"simple-git": "^3.28.0",
"string-width": "^7.1.0",
"string-width": "^8.1.0",
"strip-ansi": "^7.1.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.1",
"tar": "^7.5.2",
"tinygradient": "^1.1.5",
"undici": "^7.10.0",
"wrap-ansi": "9.0.2",
"yargs": "^17.7.2",
@@ -66,23 +68,21 @@
"devDependencies": {
"@babel/runtime": "^7.27.6",
"@google/gemini-cli-test-utils": "file:../test-utils",
"@testing-library/react": "^16.3.0",
"@types/archiver": "^6.0.3",
"@types/command-exists": "^1.2.3",
"@types/diff": "^7.0.2",
"@types/dotenv": "^6.1.1",
"@types/node": "^20.11.24",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/semver": "^7.7.0",
"@types/shell-quote": "^1.7.5",
"@types/tar": "^6.1.13",
"@types/yargs": "^17.0.32",
"archiver": "^7.0.1",
"ink-testing-library": "^4.0.0",
"jsdom": "^26.1.0",
"pretty-format": "^30.0.2",
"react-dom": "^19.1.0",
"react-dom": "^19.2.0",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
+2
View File
@@ -13,6 +13,7 @@ import { disableCommand } from './extensions/disable.js';
import { enableCommand } from './extensions/enable.js';
import { linkCommand } from './extensions/link.js';
import { newCommand } from './extensions/new.js';
import { validateCommand } from './extensions/validate.js';
export const extensionsCommand: CommandModule = {
command: 'extensions <command>',
@@ -28,6 +29,7 @@ export const extensionsCommand: CommandModule = {
.command(enableCommand)
.command(linkCommand)
.command(newCommand)
.command(validateCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {
@@ -0,0 +1,245 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleDisable, disableCommand } from './disable.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import {
loadSettings,
SettingScope,
type LoadedSettings,
} from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
vi.mock('../../config/extensions/extensionSettings.js', () => ({
promptForSetting: vi.fn(),
}));
describe('extensions disable command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockGetErrorMessage = vi.mocked(getErrorMessage);
const mockExtensionManager = vi.mocked(ExtensionManager);
beforeEach(async () => {
vi.clearAllMocks();
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue(undefined);
mockExtensionManager.prototype.disableExtension = vi
.fn()
.mockResolvedValue(undefined);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handleDisable', () => {
it.each([
{
name: 'my-extension',
scope: undefined,
expectedScope: SettingScope.User,
expectedLog:
'Extension "my-extension" successfully disabled for scope "undefined".',
},
{
name: 'my-extension',
scope: 'user',
expectedScope: SettingScope.User,
expectedLog:
'Extension "my-extension" successfully disabled for scope "user".',
},
{
name: 'my-extension',
scope: 'workspace',
expectedScope: SettingScope.Workspace,
expectedLog:
'Extension "my-extension" successfully disabled for scope "workspace".',
},
])(
'should disable an extension in the $expectedScope scope when scope is $scope',
async ({ name, scope, expectedScope, expectedLog }) => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
await handleDisable({ name, scope });
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: '/test/dir',
}),
);
expect(
mockExtensionManager.prototype.loadExtensions,
).toHaveBeenCalled();
expect(
mockExtensionManager.prototype.disableExtension,
).toHaveBeenCalledWith(name, expectedScope);
expect(emitConsoleLog).toHaveBeenCalledWith('log', expectedLog);
mockCwd.mockRestore();
},
);
it('should log an error message and exit with code 1 when extension disabling fails', async () => {
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {}) as (
code?: string | number | null | undefined,
) => never);
const error = new Error('Disable failed');
(
mockExtensionManager.prototype.disableExtension as Mock
).mockRejectedValue(error);
mockGetErrorMessage.mockReturnValue('Disable failed message');
await handleDisable({ name: 'my-extension' });
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Disable failed message',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
});
});
describe('disableCommand', () => {
const command = disableCommand as CommandModule;
it('should have correct command and describe', () => {
expect(command.command).toBe('disable [--scope] <name>');
expect(command.describe).toBe('Disables an extension.');
});
describe('builder', () => {
interface MockYargs {
positional: Mock;
option: Mock;
check: Mock;
}
let yargsMock: MockYargs;
beforeEach(() => {
yargsMock = {
positional: vi.fn().mockReturnThis(),
option: vi.fn().mockReturnThis(),
check: vi.fn().mockReturnThis(),
};
});
it('should configure positional and option arguments', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
expect(yargsMock.positional).toHaveBeenCalledWith('name', {
describe: 'The name of the extension to disable.',
type: 'string',
});
expect(yargsMock.option).toHaveBeenCalledWith('scope', {
describe: 'The scope to disable the extension in.',
type: 'string',
default: SettingScope.User,
});
expect(yargsMock.check).toHaveBeenCalled();
});
it('check function should throw for invalid scope', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
const expectedError = `Invalid scope: invalid. Please use one of ${Object.values(
SettingScope,
)
.map((s) => s.toLowerCase())
.join(', ')}.`;
expect(() => checkCallback({ scope: 'invalid' })).toThrow(
expectedError,
);
});
it.each(['user', 'workspace', 'USER', 'WorkSpace'])(
'check function should return true for valid scope "%s"',
(scope) => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
expect(checkCallback({ scope })).toBe(true);
},
);
});
it('handler should trigger extension disabling', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
interface TestArgv {
name: string;
scope: string;
[key: string]: unknown;
}
const argv: TestArgv = {
name: 'test-ext',
scope: 'workspace',
_: [],
$0: '',
};
await (command.handler as unknown as (args: TestArgv) => void)(argv);
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: '/test/dir',
}),
);
expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled();
expect(
mockExtensionManager.prototype.disableExtension,
).toHaveBeenCalledWith('test-ext', SettingScope.Workspace);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "test-ext" successfully disabled for scope "workspace".',
);
mockCwd.mockRestore();
});
});
});
@@ -76,8 +76,8 @@ export const disableCommand: CommandModule = {
}
return true;
}),
handler: (argv) => {
handleDisable({
handler: async (argv) => {
await handleDisable({
name: argv['name'] as string,
scope: argv['scope'] as string,
});
@@ -0,0 +1,213 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleEnable, enableCommand } from './enable.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import {
loadSettings,
SettingScope,
type LoadedSettings,
} from '../../config/settings.js';
import { FatalConfigError } from '@google/gemini-cli-core';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
},
debugLogger,
getErrorMessage: vi.fn((error: { message: string }) => error.message),
FatalConfigError: class extends Error {
constructor(message: string) {
super(message);
this.name = 'FatalConfigError';
}
},
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../config/extensions/consent.js');
vi.mock('../../config/extensions/extensionSettings.js');
describe('extensions enable command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockExtensionManager = vi.mocked(ExtensionManager);
beforeEach(async () => {
vi.clearAllMocks();
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue(undefined);
mockExtensionManager.prototype.enableExtension = vi.fn();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handleEnable', () => {
it.each([
{
name: 'my-extension',
scope: undefined,
expectedScope: SettingScope.User,
expectedLog:
'Extension "my-extension" successfully enabled in all scopes.',
},
{
name: 'my-extension',
scope: 'workspace',
expectedScope: SettingScope.Workspace,
expectedLog:
'Extension "my-extension" successfully enabled for scope "workspace".',
},
])(
'should enable an extension in the $expectedScope scope when scope is $scope',
async ({ name, scope, expectedScope, expectedLog }) => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
await handleEnable({ name, scope });
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: '/test/dir',
}),
);
expect(
mockExtensionManager.prototype.loadExtensions,
).toHaveBeenCalled();
expect(
mockExtensionManager.prototype.enableExtension,
).toHaveBeenCalledWith(name, expectedScope);
expect(emitConsoleLog).toHaveBeenCalledWith('log', expectedLog);
mockCwd.mockRestore();
},
);
it('should throw FatalConfigError when extension enabling fails', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
const error = new Error('Enable failed');
(
mockExtensionManager.prototype.enableExtension as Mock
).mockImplementation(() => {
throw error;
});
const promise = handleEnable({ name: 'my-extension' });
await expect(promise).rejects.toThrow(FatalConfigError);
await expect(promise).rejects.toThrow('Enable failed');
mockCwd.mockRestore();
});
});
describe('enableCommand', () => {
const command = enableCommand as CommandModule;
it('should have correct command and describe', () => {
expect(command.command).toBe('enable [--scope] <name>');
expect(command.describe).toBe('Enables an extension.');
});
describe('builder', () => {
interface MockYargs {
positional: Mock;
option: Mock;
check: Mock;
}
let yargsMock: MockYargs;
beforeEach(() => {
yargsMock = {
positional: vi.fn().mockReturnThis(),
option: vi.fn().mockReturnThis(),
check: vi.fn().mockReturnThis(),
};
});
it('should configure positional and option arguments', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
expect(yargsMock.positional).toHaveBeenCalledWith('name', {
describe: 'The name of the extension to enable.',
type: 'string',
});
expect(yargsMock.option).toHaveBeenCalledWith('scope', {
describe:
'The scope to enable the extension in. If not set, will be enabled in all scopes.',
type: 'string',
});
expect(yargsMock.check).toHaveBeenCalled();
});
it('check function should throw for invalid scope', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
const expectedError = `Invalid scope: invalid. Please use one of ${Object.values(
SettingScope,
)
.map((s) => s.toLowerCase())
.join(', ')}.`;
expect(() => checkCallback({ scope: 'invalid' })).toThrow(
expectedError,
);
});
});
it('handler should call handleEnable', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
interface TestArgv {
name: string;
scope: string;
[key: string]: unknown;
}
const argv: TestArgv = {
name: 'test-ext',
scope: 'workspace',
_: [],
$0: '',
};
await (command.handler as unknown as (args: TestArgv) => void)(argv);
expect(
mockExtensionManager.prototype.enableExtension,
).toHaveBeenCalledWith('test-ext', SettingScope.Workspace);
mockCwd.mockRestore();
});
});
});
@@ -32,9 +32,9 @@ export async function handleEnable(args: EnableArgs) {
try {
if (args.scope?.toLowerCase() === 'workspace') {
extensionManager.enableExtension(args.name, SettingScope.Workspace);
await extensionManager.enableExtension(args.name, SettingScope.Workspace);
} else {
extensionManager.enableExtension(args.name, SettingScope.User);
await extensionManager.enableExtension(args.name, SettingScope.User);
}
if (args.scope) {
debugLogger.log(
@@ -81,8 +81,8 @@ export const enableCommand: CommandModule = {
}
return true;
}),
handler: (argv) => {
handleEnable({
handler: async (argv) => {
await handleEnable({
name: argv['name'] as string,
scope: argv['scope'] as string,
});
@@ -0,0 +1,143 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// Mock the MCP server and transport
const mockRegisterTool = vi.fn();
const mockRegisterPrompt = vi.fn();
const mockConnect = vi.fn();
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
McpServer: vi.fn().mockImplementation(() => ({
registerTool: mockRegisterTool,
registerPrompt: mockRegisterPrompt,
connect: mockConnect,
})),
}));
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
StdioServerTransport: vi.fn(),
}));
describe('MCP Server Example', () => {
beforeEach(async () => {
// Dynamically import the server setup after mocks are in place
await import('./example.js');
});
afterEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
it('should create an McpServer with the correct name and version', () => {
expect(McpServer).toHaveBeenCalledWith({
name: 'prompt-server',
version: '1.0.0',
});
});
it('should register the "fetch_posts" tool', () => {
expect(mockRegisterTool).toHaveBeenCalledWith(
'fetch_posts',
{
description: 'Fetches a list of posts from a public API.',
inputSchema: z.object({}).shape,
},
expect.any(Function),
);
});
it('should register the "poem-writer" prompt', () => {
expect(mockRegisterPrompt).toHaveBeenCalledWith(
'poem-writer',
{
title: 'Poem Writer',
description: 'Write a nice haiku',
argsSchema: expect.any(Object),
},
expect.any(Function),
);
});
it('should connect the server to an StdioServerTransport', () => {
expect(StdioServerTransport).toHaveBeenCalled();
expect(mockConnect).toHaveBeenCalledWith(expect.any(StdioServerTransport));
});
describe('fetch_posts tool implementation', () => {
it('should fetch posts and return a formatted response', async () => {
const mockPosts = [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
];
global.fetch = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue(mockPosts),
});
const toolFn = (mockRegisterTool as Mock).mock.calls[0][2];
const result = await toolFn();
expect(global.fetch).toHaveBeenCalledWith(
'https://jsonplaceholder.typicode.com/posts',
);
expect(result).toEqual({
content: [
{
type: 'text',
text: JSON.stringify({ posts: mockPosts }),
},
],
});
});
});
describe('poem-writer prompt implementation', () => {
it('should generate a prompt with a title', () => {
const promptFn = (mockRegisterPrompt as Mock).mock.calls[0][2];
const result = promptFn({ title: 'My Poem' });
expect(result).toEqual({
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'Write a haiku called My Poem. Note that a haiku is 5 syllables followed by 7 syllables followed by 5 syllables ',
},
},
],
});
});
it('should generate a prompt with a title and mood', () => {
const promptFn = (mockRegisterPrompt as Mock).mock.calls[0][2];
const result = promptFn({ title: 'My Poem', mood: 'sad' });
expect(result).toEqual({
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'Write a haiku with the mood sad called My Poem. Note that a haiku is 5 syllables followed by 7 syllables followed by 5 syllables ',
},
},
],
});
});
});
});
@@ -12,7 +12,7 @@
"@types/node": "^20.11.25"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"@modelcontextprotocol/sdk": "^1.22.0",
"zod": "^3.22.4"
}
}
@@ -4,13 +4,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, type MockInstance } from 'vitest';
import { describe, it, expect, vi, type MockInstance, type Mock } from 'vitest';
import { handleInstall, installCommand } from './install.js';
import yargs from 'yargs';
import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core';
import type { ExtensionManager } from '../../config/extension-manager.js';
import type { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import type * as fs from 'node:fs/promises';
import type { Stats } from 'node:fs';
const mockInstallOrUpdateExtension = vi.hoisted(() => vi.fn());
const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn());
const mockStat = vi.hoisted(() => vi.fn());
const mockInstallOrUpdateExtension: Mock<
typeof ExtensionManager.prototype.installOrUpdateExtension
> = vi.hoisted(() => vi.fn());
const mockRequestConsentNonInteractive: Mock<
typeof requestConsentNonInteractive
> = vi.hoisted(() => vi.fn());
const mockStat: Mock<typeof fs.stat> = vi.hoisted(() => vi.fn());
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: mockRequestConsentNonInteractive,
@@ -49,13 +58,13 @@ describe('extensions install command', () => {
});
describe('handleInstall', () => {
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let debugLogSpy: MockInstance;
let debugErrorSpy: MockInstance;
let processSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log');
consoleErrorSpy = vi.spyOn(console, 'error');
debugLogSpy = vi.spyOn(debugLogger, 'log');
debugErrorSpy = vi.spyOn(debugLogger, 'error');
processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
@@ -69,37 +78,43 @@ describe('handleInstall', () => {
});
it('should install an extension from a http source', async () => {
mockInstallOrUpdateExtension.mockResolvedValue('http-extension');
mockInstallOrUpdateExtension.mockResolvedValue({
name: 'http-extension',
} as unknown as GeminiCLIExtension);
await handleInstall({
source: 'http://google.com',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
'Extension "http-extension" installed successfully and enabled.',
);
});
it('should install an extension from a https source', async () => {
mockInstallOrUpdateExtension.mockResolvedValue('https-extension');
mockInstallOrUpdateExtension.mockResolvedValue({
name: 'https-extension',
} as unknown as GeminiCLIExtension);
await handleInstall({
source: 'https://google.com',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
'Extension "https-extension" installed successfully and enabled.',
);
});
it('should install an extension from a git source', async () => {
mockInstallOrUpdateExtension.mockResolvedValue('git-extension');
mockInstallOrUpdateExtension.mockResolvedValue({
name: 'git-extension',
} as unknown as GeminiCLIExtension);
await handleInstall({
source: 'git@some-url',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
'Extension "git-extension" installed successfully and enabled.',
);
});
@@ -110,30 +125,34 @@ describe('handleInstall', () => {
source: 'test://google.com',
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Install source not found.');
expect(debugErrorSpy).toHaveBeenCalledWith('Install source not found.');
expect(processSpy).toHaveBeenCalledWith(1);
});
it('should install an extension from a sso source', async () => {
mockInstallOrUpdateExtension.mockResolvedValue('sso-extension');
mockInstallOrUpdateExtension.mockResolvedValue({
name: 'sso-extension',
} as unknown as GeminiCLIExtension);
await handleInstall({
source: 'sso://google.com',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
'Extension "sso-extension" installed successfully and enabled.',
);
});
it('should install an extension from a local path', async () => {
mockInstallOrUpdateExtension.mockResolvedValue('local-extension');
mockStat.mockResolvedValue({});
mockInstallOrUpdateExtension.mockResolvedValue({
name: 'local-extension',
} as unknown as GeminiCLIExtension);
mockStat.mockResolvedValue({} as Stats);
await handleInstall({
source: '/some/path',
});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect(debugLogSpy).toHaveBeenCalledWith(
'Extension "local-extension" installed successfully and enabled.',
);
});
@@ -145,7 +164,7 @@ describe('handleInstall', () => {
await handleInstall({ source: 'git@some-url' });
expect(consoleErrorSpy).toHaveBeenCalledWith('Install extension failed');
expect(debugErrorSpy).toHaveBeenCalledWith('Install extension failed');
expect(processSpy).toHaveBeenCalledWith(1);
});
});
@@ -77,9 +77,11 @@ export async function handleInstall(args: InstallArgs) {
settings: loadSettings(workspaceDir).merged,
});
await extensionManager.loadExtensions();
const name =
const extension =
await extensionManager.installOrUpdateExtension(installMetadata);
debugLogger.log(`Extension "${name}" installed successfully and enabled.`);
debugLogger.log(
`Extension "${extension.name}" installed successfully and enabled.`,
);
} catch (error) {
debugLogger.error(getErrorMessage(error));
process.exit(1);
@@ -0,0 +1,183 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleLink, linkCommand } from './link.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
vi.mock('../../config/extensions/extensionSettings.js', () => ({
promptForSetting: vi.fn(),
}));
describe('extensions link command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockGetErrorMessage = vi.mocked(getErrorMessage);
const mockExtensionManager = vi.mocked(ExtensionManager);
beforeEach(async () => {
vi.clearAllMocks();
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue(undefined);
mockExtensionManager.prototype.installOrUpdateExtension = vi
.fn()
.mockResolvedValue({ name: 'my-linked-extension' });
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handleLink', () => {
it('should link an extension from a local path', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
await handleLink({ path: '/local/path/to/extension' });
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: '/test/dir',
}),
);
expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled();
expect(
mockExtensionManager.prototype.installOrUpdateExtension,
).toHaveBeenCalledWith({
source: '/local/path/to/extension',
type: 'link',
});
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "my-linked-extension" linked successfully and enabled.',
);
mockCwd.mockRestore();
});
it('should log an error message and exit with code 1 when linking fails', async () => {
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {}) as (
code?: string | number | null | undefined,
) => never);
const error = new Error('Link failed');
(
mockExtensionManager.prototype.installOrUpdateExtension as Mock
).mockRejectedValue(error);
mockGetErrorMessage.mockReturnValue('Link failed message');
await handleLink({ path: '/local/path/to/extension' });
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Link failed message',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
});
});
describe('linkCommand', () => {
const command = linkCommand as CommandModule;
it('should have correct command and describe', () => {
expect(command.command).toBe('link <path>');
expect(command.describe).toBe(
'Links an extension from a local path. Updates made to the local path will always be reflected.',
);
});
describe('builder', () => {
interface MockYargs {
positional: Mock;
check: Mock;
}
let yargsMock: MockYargs;
beforeEach(() => {
yargsMock = {
positional: vi.fn().mockReturnThis(),
check: vi.fn().mockReturnThis(),
};
});
it('should configure positional argument', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
expect(yargsMock.positional).toHaveBeenCalledWith('path', {
describe: 'The name of the extension to link.',
type: 'string',
});
expect(yargsMock.check).toHaveBeenCalled();
});
});
it('handler should call handleLink', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
interface TestArgv {
path: string;
[key: string]: unknown;
}
const argv: TestArgv = {
path: '/local/path/to/extension',
_: [],
$0: '',
};
await (command.handler as unknown as (args: TestArgv) => void)(argv);
expect(
mockExtensionManager.prototype.installOrUpdateExtension,
).toHaveBeenCalledWith({
source: '/local/path/to/extension',
type: 'link',
});
mockCwd.mockRestore();
});
});
});
+2 -2
View File
@@ -34,10 +34,10 @@ export async function handleLink(args: InstallArgs) {
settings: loadSettings(workspaceDir).merged,
});
await extensionManager.loadExtensions();
const extensionName =
const extension =
await extensionManager.installOrUpdateExtension(installMetadata);
debugLogger.log(
`Extension "${extensionName}" linked successfully and enabled.`,
`Extension "${extension.name}" linked successfully and enabled.`,
);
} catch (error) {
debugLogger.error(getErrorMessage(error));
@@ -0,0 +1,139 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { type CommandModule } from 'yargs';
import { handleList, listCommand } from './list.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
vi.mock('../../config/extensions/extensionSettings.js', () => ({
promptForSetting: vi.fn(),
}));
describe('extensions list command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockGetErrorMessage = vi.mocked(getErrorMessage);
const mockExtensionManager = vi.mocked(ExtensionManager);
beforeEach(async () => {
vi.clearAllMocks();
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handleList', () => {
it('should log a message if no extensions are installed', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue([]);
await handleList();
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'No extensions installed.',
);
mockCwd.mockRestore();
});
it('should list all installed extensions', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
const extensions = [
{ name: 'ext1', version: '1.0.0' },
{ name: 'ext2', version: '2.0.0' },
];
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue(extensions);
mockExtensionManager.prototype.toOutputString = vi.fn(
(ext) => `${ext.name}@${ext.version}`,
);
await handleList();
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'ext1@1.0.0\n\next2@2.0.0',
);
mockCwd.mockRestore();
});
it('should log an error message and exit with code 1 when listing fails', async () => {
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {}) as (
code?: string | number | null | undefined,
) => never);
const error = new Error('List failed');
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockRejectedValue(error);
mockGetErrorMessage.mockReturnValue('List failed message');
await handleList();
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'List failed message',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
});
});
describe('listCommand', () => {
const command = listCommand as CommandModule;
it('should have correct command and describe', () => {
expect(command.command).toBe('list');
expect(command.describe).toBe('Lists installed extensions.');
});
it('handler should call handleList', async () => {
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue([]);
await (command.handler as () => Promise<void>)();
expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled();
});
});
});
@@ -4,18 +4,293 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { uninstallCommand } from './uninstall.js';
import yargs from 'yargs';
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleUninstall, uninstallCommand } from './uninstall.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
// NOTE: This file uses vi.hoisted() mocks to enable testing of sequential
// mock behaviors (mockResolvedValueOnce/mockRejectedValueOnce chaining).
// The hoisted mocks persist across vi.clearAllMocks() calls, which is necessary
// for testing partial failure scenarios in the multiple extension uninstall feature.
// Hoisted mocks - these survive vi.clearAllMocks()
const mockUninstallExtension = vi.hoisted(() => vi.fn());
const mockLoadExtensions = vi.hoisted(() => vi.fn());
// Mock dependencies with hoisted functions
vi.mock('../../config/extension-manager.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../config/extension-manager.js')>();
return {
...actual,
ExtensionManager: vi.fn().mockImplementation(() => ({
uninstallExtension: mockUninstallExtension,
loadExtensions: mockLoadExtensions,
setRequestConsent: vi.fn(),
setRequestSetting: vi.fn(),
})),
};
});
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
vi.mock('../../config/extensions/extensionSettings.js', () => ({
promptForSetting: vi.fn(),
}));
describe('extensions uninstall command', () => {
it('should fail if no source is provided', () => {
const validationParser = yargs([])
.command(uninstallCommand)
.fail(false)
.locale('en');
expect(() => validationParser.parse('uninstall')).toThrow(
'Not enough non-option arguments: got 0, need at least 1',
);
const mockLoadSettings = vi.mocked(loadSettings);
const mockGetErrorMessage = vi.mocked(getErrorMessage);
const mockExtensionManager = vi.mocked(ExtensionManager);
beforeEach(async () => {
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
});
afterEach(() => {
mockLoadExtensions.mockClear();
mockUninstallExtension.mockClear();
vi.clearAllMocks();
});
describe('handleUninstall', () => {
it('should uninstall a single extension', async () => {
mockLoadExtensions.mockResolvedValue(undefined);
mockUninstallExtension.mockResolvedValue(undefined);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
await handleUninstall({ names: ['my-extension'] });
expect(mockExtensionManager).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: '/test/dir',
}),
);
expect(mockLoadExtensions).toHaveBeenCalled();
expect(mockUninstallExtension).toHaveBeenCalledWith(
'my-extension',
false,
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "my-extension" successfully uninstalled.',
);
mockCwd.mockRestore();
});
it('should uninstall multiple extensions', async () => {
mockLoadExtensions.mockResolvedValue(undefined);
mockUninstallExtension.mockResolvedValue(undefined);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
await handleUninstall({ names: ['ext1', 'ext2', 'ext3'] });
expect(mockUninstallExtension).toHaveBeenCalledTimes(3);
expect(mockUninstallExtension).toHaveBeenCalledWith('ext1', false);
expect(mockUninstallExtension).toHaveBeenCalledWith('ext2', false);
expect(mockUninstallExtension).toHaveBeenCalledWith('ext3', false);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext1" successfully uninstalled.',
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext2" successfully uninstalled.',
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext3" successfully uninstalled.',
);
mockCwd.mockRestore();
});
it('should report errors for failed uninstalls but continue with others', async () => {
mockLoadExtensions.mockResolvedValue(undefined);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {}) as (
code?: string | number | null | undefined,
) => never);
const error = new Error('Extension not found');
// Chain sequential mock behaviors - this works with hoisted mocks
mockUninstallExtension
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(error)
.mockResolvedValueOnce(undefined);
mockGetErrorMessage.mockReturnValue('Extension not found');
await handleUninstall({ names: ['ext1', 'ext2', 'ext3'] });
expect(mockUninstallExtension).toHaveBeenCalledTimes(3);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext1" successfully uninstalled.',
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Failed to uninstall "ext2": Extension not found',
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext3" successfully uninstalled.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
mockCwd.mockRestore();
});
it('should exit with error code if all uninstalls fail', async () => {
mockLoadExtensions.mockResolvedValue(undefined);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {}) as (
code?: string | number | null | undefined,
) => never);
const error = new Error('Extension not found');
mockUninstallExtension.mockRejectedValue(error);
mockGetErrorMessage.mockReturnValue('Extension not found');
await handleUninstall({ names: ['ext1', 'ext2'] });
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Failed to uninstall "ext1": Extension not found',
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Failed to uninstall "ext2": Extension not found',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
mockCwd.mockRestore();
});
it('should log an error message and exit with code 1 when initialization fails', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
const mockProcessExit = vi
.spyOn(process, 'exit')
.mockImplementation((() => {}) as (
code?: string | number | null | undefined,
) => never);
const error = new Error('Initialization failed');
mockLoadExtensions.mockRejectedValue(error);
mockGetErrorMessage.mockReturnValue('Initialization failed message');
await handleUninstall({ names: ['my-extension'] });
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Initialization failed message',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
mockProcessExit.mockRestore();
mockCwd.mockRestore();
});
});
describe('uninstallCommand', () => {
const command = uninstallCommand as CommandModule;
it('should have correct command and describe', () => {
expect(command.command).toBe('uninstall <names..>');
expect(command.describe).toBe('Uninstalls one or more extensions.');
});
describe('builder', () => {
interface MockYargs {
positional: Mock;
check: Mock;
}
let yargsMock: MockYargs;
beforeEach(() => {
yargsMock = {
positional: vi.fn().mockReturnThis(),
check: vi.fn().mockReturnThis(),
};
});
it('should configure positional argument', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
expect(yargsMock.positional).toHaveBeenCalledWith('names', {
describe:
'The name(s) or source path(s) of the extension(s) to uninstall.',
type: 'string',
array: true,
});
expect(yargsMock.check).toHaveBeenCalled();
});
it('check function should throw for missing names', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
expect(() => checkCallback({ names: [] })).toThrow(
'Please include at least one extension name to uninstall as a positional argument.',
);
});
});
it('handler should call handleUninstall', async () => {
mockLoadExtensions.mockResolvedValue(undefined);
mockUninstallExtension.mockResolvedValue(undefined);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
interface TestArgv {
names: string[];
[key: string]: unknown;
}
const argv: TestArgv = { names: ['my-extension'], _: [], $0: '' };
await (command.handler as unknown as (args: TestArgv) => void)(argv);
expect(mockUninstallExtension).toHaveBeenCalledWith(
'my-extension',
false,
);
mockCwd.mockRestore();
});
});
});
@@ -13,7 +13,7 @@ import { loadSettings } from '../../config/settings.js';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
interface UninstallArgs {
name: string; // can be extension name or source URL.
names: string[]; // can be extension names or source URLs.
}
export async function handleUninstall(args: UninstallArgs) {
@@ -26,8 +26,23 @@ export async function handleUninstall(args: UninstallArgs) {
settings: loadSettings(workspaceDir).merged,
});
await extensionManager.loadExtensions();
await extensionManager.uninstallExtension(args.name, false);
debugLogger.log(`Extension "${args.name}" successfully uninstalled.`);
const errors: Array<{ name: string; error: string }> = [];
for (const name of [...new Set(args.names)]) {
try {
await extensionManager.uninstallExtension(name, false);
debugLogger.log(`Extension "${name}" successfully uninstalled.`);
} catch (error) {
errors.push({ name, error: getErrorMessage(error) });
}
}
if (errors.length > 0) {
for (const { name, error } of errors) {
debugLogger.error(`Failed to uninstall "${name}": ${error}`);
}
process.exit(1);
}
} catch (error) {
debugLogger.error(getErrorMessage(error));
process.exit(1);
@@ -35,25 +50,27 @@ export async function handleUninstall(args: UninstallArgs) {
}
export const uninstallCommand: CommandModule = {
command: 'uninstall <name>',
describe: 'Uninstalls an extension.',
command: 'uninstall <names..>',
describe: 'Uninstalls one or more extensions.',
builder: (yargs) =>
yargs
.positional('name', {
describe: 'The name or source path of the extension to uninstall.',
.positional('names', {
describe:
'The name(s) or source path(s) of the extension(s) to uninstall.',
type: 'string',
array: true,
})
.check((argv) => {
if (!argv.name) {
if (!argv.names || (argv.names as string[]).length === 0) {
throw new Error(
'Please include the name of the extension to uninstall as a positional argument.',
'Please include at least one extension name to uninstall as a positional argument.',
);
}
return true;
}),
handler: async (argv) => {
await handleUninstall({
name: argv['name'] as string,
names: argv['names'] as string[],
});
},
};
@@ -0,0 +1,230 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { type CommandModule, type Argv } from 'yargs';
import { handleUpdate, updateCommand } from './update.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import * as update from '../../config/extensions/update.js';
import * as github from '../../config/extensions/github.js';
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/update.js');
vi.mock('../../config/extensions/github.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
vi.mock('../../config/extensions/extensionSettings.js', () => ({
promptForSetting: vi.fn(),
}));
describe('extensions update command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockExtensionManager = vi.mocked(ExtensionManager);
const mockUpdateExtension = vi.mocked(update.updateExtension);
const mockCheckForExtensionUpdate = vi.mocked(github.checkForExtensionUpdate);
const mockCheckForAllExtensionUpdates = vi.mocked(
update.checkForAllExtensionUpdates,
);
const mockUpdateAllUpdatableExtensions = vi.mocked(
update.updateAllUpdatableExtensions,
);
beforeEach(async () => {
vi.clearAllMocks();
mockLoadSettings.mockReturnValue({
merged: { experimental: { extensionReloading: true } },
} as unknown as LoadedSettings);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handleUpdate', () => {
it.each([
{
state: ExtensionUpdateState.UPDATE_AVAILABLE,
expectedLog:
'Extension "my-extension" successfully updated: 1.0.0 → 1.1.0.',
shouldCallUpdateExtension: true,
},
{
state: ExtensionUpdateState.UP_TO_DATE,
expectedLog: 'Extension "my-extension" is already up to date.',
shouldCallUpdateExtension: false,
},
])(
'should handle single extension update state: $state',
async ({ state, expectedLog, shouldCallUpdateExtension }) => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
const extensions = [{ name: 'my-extension', installMetadata: {} }];
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue(extensions);
mockCheckForExtensionUpdate.mockResolvedValue(state);
mockUpdateExtension.mockResolvedValue({
name: 'my-extension',
originalVersion: '1.0.0',
updatedVersion: '1.1.0',
});
await handleUpdate({ name: 'my-extension' });
expect(emitConsoleLog).toHaveBeenCalledWith('log', expectedLog);
if (shouldCallUpdateExtension) {
expect(mockUpdateExtension).toHaveBeenCalled();
} else {
expect(mockUpdateExtension).not.toHaveBeenCalled();
}
mockCwd.mockRestore();
},
);
it.each([
{
updatedExtensions: [
{ name: 'ext1', originalVersion: '1.0.0', updatedVersion: '1.1.0' },
{ name: 'ext2', originalVersion: '2.0.0', updatedVersion: '2.1.0' },
],
expectedLog:
'Extension "ext1" successfully updated: 1.0.0 → 1.1.0.\nExtension "ext2" successfully updated: 2.0.0 → 2.1.0.',
},
{
updatedExtensions: [],
expectedLog: 'No extensions to update.',
},
])(
'should handle updating all extensions: %s',
async ({ updatedExtensions, expectedLog }) => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue([]);
mockCheckForAllExtensionUpdates.mockResolvedValue(undefined);
mockUpdateAllUpdatableExtensions.mockResolvedValue(updatedExtensions);
await handleUpdate({ all: true });
expect(emitConsoleLog).toHaveBeenCalledWith('log', expectedLog);
mockCwd.mockRestore();
},
);
});
describe('updateCommand', () => {
const command = updateCommand as CommandModule;
it('should have correct command and describe', () => {
expect(command.command).toBe('update [<name>] [--all]');
expect(command.describe).toBe(
'Updates all extensions or a named extension to the latest version.',
);
});
describe('builder', () => {
interface MockYargs {
positional: Mock;
option: Mock;
conflicts: Mock;
check: Mock;
}
let yargsMock: MockYargs;
beforeEach(() => {
yargsMock = {
positional: vi.fn().mockReturnThis(),
option: vi.fn().mockReturnThis(),
conflicts: vi.fn().mockReturnThis(),
check: vi.fn().mockReturnThis(),
};
});
it('should configure arguments', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
expect(yargsMock.positional).toHaveBeenCalledWith(
'name',
expect.any(Object),
);
expect(yargsMock.option).toHaveBeenCalledWith(
'all',
expect.any(Object),
);
expect(yargsMock.conflicts).toHaveBeenCalledWith('name', 'all');
expect(yargsMock.check).toHaveBeenCalled();
});
it('check function should throw an error if neither a name nor --all is provided', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
expect(() => checkCallback({ name: undefined, all: false })).toThrow(
'Either an extension name or --all must be provided',
);
});
});
it('handler should call handleUpdate', async () => {
const extensions = [{ name: 'my-extension', installMetadata: {} }];
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue(extensions);
mockCheckForExtensionUpdate.mockResolvedValue(
ExtensionUpdateState.UPDATE_AVAILABLE,
);
mockUpdateExtension.mockResolvedValue({
name: 'my-extension',
originalVersion: '1.0.0',
updatedVersion: '1.1.0',
});
await (command.handler as (args: object) => Promise<void>)({
name: 'my-extension',
});
expect(mockUpdateExtension).toHaveBeenCalled();
});
});
});
@@ -30,11 +30,12 @@ const updateOutput = (info: ExtensionUpdateInfo) =>
export async function handleUpdate(args: UpdateArgs) {
const workspaceDir = process.cwd();
const settings = loadSettings(workspaceDir).merged;
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent: requestConsentNonInteractive,
requestSetting: promptForSetting,
settings: loadSettings(workspaceDir).merged,
settings,
});
const extensions = await extensionManager.loadExtensions();
@@ -67,6 +68,7 @@ export async function handleUpdate(args: UpdateArgs) {
extensionManager,
updateState,
() => {},
settings.experimental?.extensionReloading,
))!;
if (
updatedExtensionInfo.originalVersion !==
@@ -0,0 +1,122 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import { describe, it, expect, vi, type MockInstance } from 'vitest';
import { handleValidate, validateCommand } from './validate.js';
import yargs from 'yargs';
import { createExtension } from '../../test-utils/createExtension.js';
import path from 'node:path';
import * as os from 'node:os';
import { debugLogger } from '@google/gemini-cli-core';
describe('extensions validate command', () => {
it('should fail if no path is provided', () => {
const validationParser = yargs([]).command(validateCommand).fail(false);
expect(() => validationParser.parse('validate')).toThrow(
'Not enough non-option arguments: got 0, need at least 1',
);
});
});
describe('handleValidate', () => {
let debugLoggerLogSpy: MockInstance;
let debugLoggerWarnSpy: MockInstance;
let debugLoggerErrorSpy: MockInstance;
let processSpy: MockInstance;
let tempHomeDir: string;
let tempWorkspaceDir: string;
beforeEach(() => {
debugLoggerLogSpy = vi.spyOn(debugLogger, 'log');
debugLoggerWarnSpy = vi.spyOn(debugLogger, 'warn');
debugLoggerErrorSpy = vi.spyOn(debugLogger, 'error');
processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-home'));
tempWorkspaceDir = fs.mkdtempSync(path.join(tempHomeDir, 'test-workspace'));
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(tempHomeDir, { recursive: true, force: true });
fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
});
it('should validate an extension from a local dir', async () => {
createExtension({
extensionsDir: tempWorkspaceDir,
name: 'local-ext-name',
version: '1.0.0',
});
await handleValidate({
path: 'local-ext-name',
});
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'Extension local-ext-name has been successfully validated.',
);
});
it('should throw an error if the extension name is invalid', async () => {
createExtension({
extensionsDir: tempWorkspaceDir,
name: 'INVALID_NAME',
version: '1.0.0',
});
await handleValidate({
path: 'INVALID_NAME',
});
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Invalid extension name: "INVALID_NAME". Only letters (a-z, A-Z), numbers (0-9), and dashes (-) are allowed.',
),
);
expect(processSpy).toHaveBeenCalledWith(1);
});
it('should warn if version is not formatted with semver', async () => {
createExtension({
extensionsDir: tempWorkspaceDir,
name: 'valid-name',
version: '1',
});
await handleValidate({
path: 'valid-name',
});
expect(debugLoggerWarnSpy).toHaveBeenCalledWith(
expect.stringContaining(
"Version '1' does not appear to be standard semver (e.g., 1.0.0).",
),
);
expect(debugLoggerLogSpy).toHaveBeenCalledWith(
'Extension valid-name has been successfully validated.',
);
});
it('should throw an error if context files are missing', async () => {
createExtension({
extensionsDir: tempWorkspaceDir,
name: 'valid-name',
version: '1.0.0',
contextFileName: 'contextFile.md',
});
fs.rmSync(path.join(tempWorkspaceDir, 'valid-name/contextFile.md'));
await handleValidate({
path: 'valid-name',
});
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(
'The following context files referenced in gemini-extension.json are missing: contextFile.md',
),
);
expect(processSpy).toHaveBeenCalledWith(1);
});
});
@@ -0,0 +1,105 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import { debugLogger } from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import semver from 'semver';
import { getErrorMessage } from '../../utils/errors.js';
import type { ExtensionConfig } from '../../config/extension.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
import { loadSettings } from '../../config/settings.js';
interface ValidateArgs {
path: string;
}
export async function handleValidate(args: ValidateArgs) {
try {
await validateExtension(args);
debugLogger.log(`Extension ${args.path} has been successfully validated.`);
} catch (error) {
debugLogger.error(getErrorMessage(error));
process.exit(1);
}
}
async function validateExtension(args: ValidateArgs) {
const workspaceDir = process.cwd();
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent: requestConsentNonInteractive,
requestSetting: promptForSetting,
settings: loadSettings(workspaceDir).merged,
});
const absoluteInputPath = path.resolve(args.path);
const extensionConfig: ExtensionConfig =
extensionManager.loadExtensionConfig(absoluteInputPath);
const warnings: string[] = [];
const errors: string[] = [];
if (extensionConfig.contextFileName) {
const contextFileNames = Array.isArray(extensionConfig.contextFileName)
? extensionConfig.contextFileName
: [extensionConfig.contextFileName];
const missingContextFiles: string[] = [];
for (const contextFilePath of contextFileNames) {
const contextFileAbsolutePath = path.resolve(
absoluteInputPath,
contextFilePath,
);
if (!fs.existsSync(contextFileAbsolutePath)) {
missingContextFiles.push(contextFilePath);
}
}
if (missingContextFiles.length > 0) {
errors.push(
`The following context files referenced in gemini-extension.json are missing: ${missingContextFiles}`,
);
}
}
if (!semver.valid(extensionConfig.version)) {
warnings.push(
`Warning: Version '${extensionConfig.version}' does not appear to be standard semver (e.g., 1.0.0).`,
);
}
if (warnings.length > 0) {
debugLogger.warn('Validation warnings:');
for (const warning of warnings) {
debugLogger.warn(` - ${warning}`);
}
}
if (errors.length > 0) {
debugLogger.error('Validation failed with the following errors:');
for (const error of errors) {
debugLogger.error(` - ${error}`);
}
throw new Error('Extension validation failed.');
}
}
export const validateCommand: CommandModule = {
command: 'validate <path>',
describe: 'Validates an extension from a local path.',
builder: (yargs) =>
yargs.positional('path', {
describe: 'The path of the extension to validate.',
type: 'string',
demandOption: true,
}),
handler: async (args) => {
await handleValidate({
path: args['path'] as string,
});
},
};
+9 -4
View File
@@ -4,10 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, type Mock } from 'vitest';
import { describe, it, expect, vi, type Mock, type MockInstance } from 'vitest';
import yargs, { type Argv } from 'yargs';
import { addCommand } from './add.js';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { debugLogger } from '@google/gemini-cli-core';
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
@@ -38,6 +39,7 @@ describe('mcp add command', () => {
let parser: Argv;
let mockSetValue: Mock;
let mockConsoleError: Mock;
let debugLoggerErrorSpy: MockInstance;
beforeEach(() => {
vi.resetAllMocks();
@@ -45,6 +47,9 @@ describe('mcp add command', () => {
parser = yargsInstance;
mockSetValue = vi.fn();
mockConsoleError = vi.fn();
debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(mockConsoleError);
mockedLoadSettings.mockReturnValue({
forScope: () => ({ settings: {} }),
@@ -232,7 +237,7 @@ describe('mcp add command', () => {
parser.parseAsync(`add ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -250,7 +255,7 @@ describe('mcp add command', () => {
parser.parseAsync(`add --scope project ${serverName} ${command}`),
).rejects.toThrow('process.exit called');
expect(mockConsoleError).toHaveBeenCalledWith(
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Error: Please use --scope user to edit settings in the home directory.',
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -264,7 +269,7 @@ describe('mcp add command', () => {
'mcpServers',
expect.any(Object),
);
expect(mockConsoleError).not.toHaveBeenCalled();
expect(debugLoggerErrorSpy).not.toHaveBeenCalled();
});
});
+22 -21
View File
@@ -21,27 +21,27 @@ vi.mock('../../config/extensions/storage.js', () => ({
},
}));
vi.mock('../../config/extension-manager.js');
vi.mock('@google/gemini-cli-core', () => ({
createTransport: vi.fn(),
MCPServerStatus: {
CONNECTED: 'CONNECTED',
CONNECTING: 'CONNECTING',
DISCONNECTED: 'DISCONNECTED',
},
Storage: vi.fn().mockImplementation((_cwd: string) => ({
getGlobalSettingsPath: () => '/tmp/gemini/settings.json',
getWorkspaceSettingsPath: () => '/tmp/gemini/workspace-settings.json',
getProjectTempDir: () => '/test/home/.gemini/tmp/mocked_hash',
})),
GEMINI_DIR: '.gemini',
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
debugLogger: {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...original,
createTransport: vi.fn(),
MCPServerStatus: {
CONNECTED: 'CONNECTED',
CONNECTING: 'CONNECTING',
DISCONNECTED: 'DISCONNECTED',
},
Storage: vi.fn().mockImplementation((_cwd: string) => ({
getGlobalSettingsPath: () => '/tmp/gemini/settings.json',
getWorkspaceSettingsPath: () => '/tmp/gemini/workspace-settings.json',
getProjectTempDir: () => '/test/home/.gemini/tmp/mocked_hash',
})),
GEMINI_DIR: '.gemini',
getErrorMessage: (e: unknown) =>
e instanceof Error ? e.message : String(e),
};
});
vi.mock('@modelcontextprotocol/sdk/client/index.js');
const mockedGetUserExtensionsDir =
@@ -72,6 +72,7 @@ describe('mcp list command', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
mockTransport = { close: vi.fn() };
mockClient = {

Some files were not shown because too many files have changed in this diff Show More