Compare commits

..

235 Commits

Author SHA1 Message Date
gemini-cli-robot a2b66aead3 chore(release): v0.16.0-preview.3 2025-11-15 01:44:58 +00:00
gemini-cli-robot 6f34e25892 fix(patch): cherry-pick ab6b229 to release/v0.16.0-preview.2-pr-13101 to patch version v0.16.0-preview.2 and create version 0.16.0-preview.3 (#13110)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2025-11-14 17:31:09 -08:00
gemini-cli-robot dcc2a49935 chore(release): v0.16.0-preview.2 2025-11-14 22:16:31 +00:00
gemini-cli-robot cc608b9a97 fix(patch): cherry-pick ce56b4e to release/v0.16.0-preview.1-pr-13073 to patch version v0.16.0-preview.1 and create version 0.16.0-preview.2 (#13097)
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
2025-11-14 17:01:55 -05:00
gemini-cli-robot ea4cd98e2d chore(release): v0.16.0-preview.1 2025-11-14 21:27:03 +00:00
gemini-cli-robot 3cb670fe3d fix(patch): cherry-pick ba15eeb to release/v0.16.0-preview.0-pr-13083 to patch version v0.16.0-preview.0 and create version 0.16.0-preview.1 (#13093)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2025-11-14 13:11:02 -08:00
gemini-cli-robot 1c65689257 chore(release): v0.16.0-preview.0 2025-11-13 23:54:15 +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
594 changed files with 48430 additions and 17538 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:
-47
View File
@@ -1,47 +0,0 @@
# .github/actions/publish-bundle/action.yml
name: 'Publish Bundle'
description: 'Builds, versions, and publishes the bundled CLI package.'
inputs:
version:
description: 'The version to publish the package with.'
required: true
tag:
description: 'The npm dist-tag to publish the package with.'
required: true
github-token:
description: 'GitHub token for authentication.'
required: true
package-scope:
description: 'The npm scope to publish the package to.'
required: false
runs:
using: 'composite'
steps:
- name: 'Setup NPMRC'
uses: './.github/actions/setup-npmrc'
with:
github-token: '${{ inputs.github-token }}'
- name: 'Bundle'
shell: 'bash'
run: 'npm run bundle'
- name: 'Prepare Package for Publishing'
if: "${{ inputs.package-scope != '' }}"
shell: 'bash'
env:
PACKAGE_SCOPE: '${{ inputs.package-scope }}'
run: 'node scripts/prepare-package.js --scope=${{ env.PACKAGE_SCOPE }}'
- name: 'Set Version'
shell: 'bash'
env:
PACKAGE_VERSION: '${{ inputs.version }}'
run: 'npm version --no-git-tag-version "${{ env.PACKAGE_VERSION }}"'
- name: 'Publish to GitHub Packages'
env:
NODE_AUTH_TOKEN: '${{ inputs.github-token }}'
NPM_TAG: '${{ inputs.tag }}'
shell: 'bash'
run: 'npm publish --tag="${{ env.NPM_TAG }}"'
@@ -1,274 +0,0 @@
name: 'Publish Release (DEPRECATED)'
description: 'Builds, prepares, and publishes the gemini-cli packages to npm and creates a GitHub release.'
inputs:
release-version:
description: 'The version to release (e.g., 0.1.11).'
required: true
npm-tag:
description: 'The npm tag to publish with (e.g., latest, preview, nightly).'
required: true
wombat-token-core:
description: 'The npm token for the cli-core package.'
required: true
wombat-token-cli:
description: 'The npm token for the cli package.'
required: true
wombat-token-a2a-server:
description: 'The npm token for the a2a package.'
required: true
github-token:
description: 'The GitHub token for creating the release.'
required: true
dry-run:
description: 'Whether to run in dry-run mode.'
type: 'string'
required: true
release-tag:
description: 'The release tag for the release (e.g., v0.1.11).'
required: true
previous-tag:
description: 'The previous tag to use for generating release notes.'
required: true
skip-github-release:
description: 'Whether to skip creating a GitHub release.'
type: 'boolean'
required: false
default: false
working-directory:
description: 'The working directory to run the steps in.'
required: false
default: '.'
force-skip-tests:
description: 'Skip tests and validation'
required: false
default: false
skip-branch-cleanup:
description: 'Whether to skip cleaning up the release branch.'
type: 'boolean'
required: false
default: false
gemini_api_key:
description: 'The API key for running integration tests.'
required: true
npm-registry-publish-url:
description: 'npm registry publish url'
required: true
npm-registry-url:
description: 'npm registry url'
required: true
npm-registry-scope:
description: 'npm registry scope'
required: true
cli-package-name:
description: 'The name of the cli package.'
required: true
core-package-name:
description: 'The name of the core package.'
required: true
a2a-package-name:
description: 'The name of the a2a package.'
required: true
runs:
using: 'composite'
steps:
- name: '📝 Print Inputs'
shell: 'bash'
env:
JSON_INPUTS: '${{ toJSON(inputs) }}'
run: 'echo "$JSON_INPUTS"'
- name: '👤 Configure Git User'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
git config user.name "gemini-cli-robot"
git config user.email "gemini-cli-robot@google.com"
- name: '🌿 Create and switch to a release branch'
working-directory: '${{ inputs.working-directory }}'
id: 'release_branch'
shell: 'bash'
run: |
BRANCH_NAME="release/${{ inputs.release-tag }}"
git switch -c "${BRANCH_NAME}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
- name: '⬆️ Update package versions'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
npm run release:version "${{ inputs.release-version }}"
- name: '💾 Commit and Conditionally Push package versions'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
env:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
DRY_RUN: '${{ inputs.dry-run }}'
RELEASE_TAG: '${{ inputs.release-tag }}'
run: |-
set -e
git add package.json package-lock.json packages/*/package.json
git commit -m "chore(release): ${RELEASE_TAG}"
if [[ "${DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..."
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
else
echo "Dry run enabled. Skipping push."
fi
- name: '🛠️ Build and Prepare Packages'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
npm run build:packages
npm run prepare:package
- name: '🎁 Bundle'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
npm run bundle
# TODO: Refactor this github specific publishing script to be generalized based upon inputs.
- name: '📦 Prepare for GitHub release'
if: "inputs.npm-registry-url == 'https://npm.pkg.github.com/'"
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
node ${{ github.workspace }}/scripts/prepare-github-release.js
- name: 'Configure npm for publishing to npm'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version-file: '${{ inputs.working-directory }}/.nvmrc'
registry-url: '${{inputs.npm-registry-publish-url}}'
scope: '${{inputs.npm-registry-scope}}'
- name: 'Get core Token'
uses: './.github/actions/npm-auth-token'
id: 'core-token'
with:
package-name: '${{ inputs.core-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: '📦 Publish CORE to NPM'
working-directory: '${{ inputs.working-directory }}'
env:
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
shell: 'bash'
run: |
npm publish \
--dry-run="${{ inputs.dry-run }}" \
--workspace="${{ inputs.core-package-name }}" \
--no-tag
npm dist-tag rm ${{ inputs.core-package-name }} false --silent
- name: '🔗 Install latest core package'
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run != 'true' }}"
shell: 'bash'
run: |
npm install "${{ inputs.core-package-name }}@${{ inputs.release-version }}" \
--workspace="${{ inputs.cli-package-name }}" \
--workspace="${{ inputs.a2a-package-name }}" \
--save-exact
- name: 'Get CLI Token'
uses: './.github/actions/npm-auth-token'
id: 'cli-token'
with:
package-name: '${{ inputs.cli-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: '📦 Publish CLI'
working-directory: '${{ inputs.working-directory }}'
env:
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
shell: 'bash'
run: |
npm publish \
--dry-run="${{ inputs.dry-run }}" \
--workspace="${{ inputs.cli-package-name }}" \
--no-tag
npm dist-tag rm ${{ inputs.cli-package-name }} false --silent
- name: 'Get a2a-server Token'
uses: './.github/actions/npm-auth-token'
id: 'a2a-token'
with:
package-name: '${{ inputs.a2a-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: '📦 Publish a2a'
working-directory: '${{ inputs.working-directory }}'
env:
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
shell: 'bash'
# Tag staging for initial release
run: |
npm publish \
--dry-run="${{ inputs.dry-run }}" \
--workspace="${{ inputs.a2a-package-name }}" \
--no-tag
npm dist-tag rm ${{ inputs.a2a-package-name }} false --silent
- name: '🔬 Verify NPM release by version'
uses: './.github/actions/verify-release'
if: "${{ inputs.dry-run != 'true' && inputs.force-skip-tests != 'true' }}"
with:
npm-package: '${{ inputs.cli-package-name }}@${{ inputs.release-version }}'
expected-version: '${{ inputs.release-version }}'
working-directory: '${{ inputs.working-directory }}'
gemini_api_key: '${{ inputs.gemini_api_key }}'
github-token: '${{ inputs.github-token }}'
npm-registry-url: '${{ inputs.npm-registry-url }}'
npm-registry-scope: '${{ inputs.npm-registry-scope }}'
- name: '🏷️ Tag release'
uses: './.github/actions/tag-npm-release'
with:
channel: '${{ inputs.npm-tag }}'
version: '${{ inputs.release-version }}'
dry-run: '${{ inputs.dry-run }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
cli-package-name: '${{ inputs.cli-package-name }}'
core-package-name: '${{ inputs.core-package-name }}'
a2a-package-name: '${{ inputs.a2a-package-name }}'
working-directory: '${{ inputs.working-directory }}'
- name: '🎉 Create GitHub Release'
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run != 'true' && inputs.skip-github-release != 'true' && inputs.npm-tag != 'dev' && inputs.npm-registry-url != 'https://npm.pkg.github.com/' }}"
env:
GITHUB_TOKEN: '${{ inputs.github-token }}'
shell: 'bash'
run: |
gh release create "${{ inputs.release-tag }}" \
bundle/gemini.js \
--target "${{ steps.release_branch.outputs.BRANCH_NAME }}" \
--title "Release ${{ inputs.release-tag }}" \
--notes-start-tag "${{ inputs.previous-tag }}" \
--generate-notes
- name: '🧹 Clean up release branch'
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run != 'true' && inputs.skip-branch-cleanup != 'true' }}"
continue-on-error: true
shell: 'bash'
run: |
echo "Cleaning up release branch ${{ steps.release_branch.outputs.BRANCH_NAME }}..."
git push origin --delete "${{ steps.release_branch.outputs.BRANCH_NAME }}"
+1 -1
View File
@@ -46,7 +46,7 @@ runs:
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
with:
registry: 'ghcr.io'
username: '${{ github.repository_owner }}'
username: '${{ inputs.github-actor }}'
password: '${{ inputs.github-secret }}'
- name: 'Get branch name'
id: 'branch_name'
+96
View File
@@ -0,0 +1,96 @@
name: 'Build and push sandbox docker'
description: 'Pushes sandbox docker image to container registry'
inputs:
github-actor:
description: 'Github actor'
required: true
github-secret:
description: 'Github secret'
required: true
dockerhub-username:
description: 'Dockerhub username'
required: true
dockerhub-token:
description: 'Dockerhub PAT w/ R+W'
required: true
github-sha:
description: 'Github Commit SHA Hash'
required: true
github-ref-name:
description: 'Github ref name'
required: true
dry-run:
description: 'Whether this is a dry run.'
required: true
type: 'boolean'
runs:
using: 'composite'
steps:
- name: '📝 Print Inputs'
shell: 'bash'
env:
JSON_INPUTS: '${{ toJSON(inputs) }}'
run: 'echo "$JSON_INPUTS"'
- name: 'Checkout'
uses: 'actions/checkout@v4'
with:
ref: '${{ inputs.github-sha }}'
fetch-depth: 0
- name: 'Install Dependencies'
shell: 'bash'
run: 'npm install'
- name: 'npm build'
shell: 'bash'
run: 'npm run build'
- name: 'Set up Docker Buildx'
uses: 'docker/setup-buildx-action@v3'
- name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@v3'
with:
registry: 'docker.io'
username: '${{ inputs.dockerhub-username }}'
password: '${{ inputs.dockerhub-token }}'
- name: 'determine image tag'
id: 'image_tag'
shell: 'bash'
run: |-
SHELL_TAG_NAME="${{ inputs.github-ref-name }}"
FINAL_TAG="${{ inputs.github-sha }}"
if [[ "$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "Release detected."
FINAL_TAG="${SHELL_TAG_NAME#v}"
else
echo "Development release detected. Using commit SHA as tag."
fi
echo "Determined image tag: $FINAL_TAG"
echo "FINAL_TAG=$FINAL_TAG" >> $GITHUB_OUTPUT
- name: 'build'
id: 'docker_build'
shell: 'bash'
env:
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
GEMINI_SANDBOX: 'docker'
run: |-
npm run build:sandbox -- \
--image google/gemini-cli-sandbox:${{ steps.image_tag.outputs.FINAL_TAG }} \
--output-file final_image_uri.txt
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
- name: 'publish'
shell: 'bash'
if: "${{ inputs.dry-run != 'true' }}"
run: |-
docker push "${{ steps.docker_build.outputs.uri }}"
- name: 'Create issue on failure'
if: |-
${{ failure() }}
shell: 'bash'
env:
GITHUB_TOKEN: '${{ inputs.github-secret }}'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |-
gh issue create \
--title "Docker build failed" \
--body "The docker build failed. See the full run for details: ${DETAILS_URL}" \
--label "kind/bug,release-failure"
-124
View File
@@ -1,124 +0,0 @@
# .github/workflows/build-and-publish.yml
name: 'Build and Publish Bundle'
on:
workflow_call:
inputs:
cli-package-name:
description: 'The name of the CLI package.'
required: true
type: 'string'
npm-registry-scope:
description: 'NPM registry scope'
required: true
type: 'string'
npm-registry-url:
description: 'NPM registry URL'
required: true
type: 'string'
outputs:
version:
description: 'The version of the published package'
value: '${{ jobs.publish-bundle.outputs.version }}'
image-uri:
description: 'The URI of the built sandbox image.'
value: '${{ jobs.build-and-publish-sandbox.outputs.image-uri }}'
jobs:
publish-bundle:
name: 'Publish Bundle'
runs-on: 'ubuntu-latest'
outputs:
version: '${{ steps.vars.outputs.version }}'
permissions:
contents: 'read'
packages: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 0
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install Dependencies'
run: 'npm ci'
- name: 'Calculate Version and Tag'
id: 'vars'
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
NEW_VERSION="${CURRENT_VERSION}-ci.${{ github.run_number }}.${{ github.sha }}"
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "tag=pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
else
echo "tag=ci" >> "$GITHUB_OUTPUT"
fi
- name: 'Publish Bundle'
uses: './.github/actions/publish-bundle'
with:
version: '${{ steps.vars.outputs.version }}'
tag: '${{ steps.vars.outputs.tag }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
package-scope: '${{ inputs.npm-registry-scope }}'
build-and-publish-sandbox:
name: 'Build and Publish Sandbox'
runs-on: 'ubuntu-latest'
needs: 'publish-bundle'
outputs:
image-uri: '${{ steps.docker_build.outputs.uri }}'
permissions:
contents: 'read'
packages: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 0
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Setup NPMRC'
uses: './.github/actions/setup-npmrc'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@v3'
with:
registry: 'ghcr.io'
username: '${{ github.repository_owner }}'
password: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Set up Docker Buildx'
uses: 'docker/setup-buildx-action@v3'
- name: 'Build and Push Sandbox Image'
id: 'docker_build'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
shell: 'bash'
run: |
IMAGE_TAG="ghcr.io/${{ github.repository }}/sandbox:${{ needs.publish-bundle.outputs.version }}"
docker build . \
--file Dockerfile \
--tag "${IMAGE_TAG}" \
--build-arg CLI_VERSION=${{ needs.publish-bundle.outputs.version }} \
--build-arg NPM_REGISTRY_SCOPE=${{ inputs.npm-registry-scope }} \
--build-arg NPM_REGISTRY_URL=${{ inputs.npm-registry-url }} \
--build-arg CLI_PACKAGE_NAME=${{ inputs.cli-package-name }} \
--secret id=GITHUB_TOKEN
docker push "${IMAGE_TAG}"
echo "uri=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
-50
View File
@@ -1,50 +0,0 @@
# .github/workflows/bundle-size.yml
name: 'Bundle Size Check'
on:
workflow_call:
inputs:
version:
description: 'The version of the published package'
required: true
type: 'string'
permissions:
contents: 'read'
pull-requests: 'write'
packages: 'read'
jobs:
bundle-size:
name: 'Check Bundle Size'
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Setup NPMRC'
uses: './.github/actions/setup-npmrc'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Install dependencies'
env:
VERSION: '${{ inputs.version }}'
run: 'npm install @google-gemini/gemini-cli@${{ env.VERSION }}'
- name: 'Check Bundle Size'
uses: 'preactjs/compressed-size-action@946a292cd35bd1088e0d7eb92b69d1a8d5b5d76a'
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './bundle/**/*.{js,sb}'
minimum-change-threshold: '1000'
compression: 'none'
clean-script: 'clean'
+195 -17
View File
@@ -1,27 +1,127 @@
name: 'Testing: CI'
on:
workflow_call:
push:
branches:
- 'main'
- 'release/**'
pull_request:
branches:
- 'main'
- 'release/**'
merge_group:
workflow_dispatch:
inputs:
branch_ref:
description: 'Branch to run on'
required: true
default: 'main'
type: 'string'
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: |-
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
permissions:
checks: 'write'
contents: 'read'
statuses: 'write'
packages: 'read'
pull-requests: 'write'
actions: 'read'
security-events: 'write'
defaults:
run:
shell: 'bash'
jobs:
merge_queue_skipper:
permissions: 'read-all'
name: 'Merge Queue Skipper'
runs-on: 'gemini-cli-ubuntu-16-core'
outputs:
skip: '${{ steps.merge-queue-ci-skipper.outputs.skip-check }}'
steps:
- id: 'merge-queue-ci-skipper'
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
with:
secret: '${{ secrets.GITHUB_TOKEN }}'
lint:
name: 'Lint'
runs-on: 'gemini-cli-ubuntu-16-core'
needs: 'merge_queue_skipper'
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 0
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Validate NOTICES.txt'
run: 'git diff --exit-code packages/vscode-ide-companion/NOTICES.txt'
- name: 'Check lockfile'
run: 'npm run check:lockfile'
- name: 'Install linters'
run: 'node scripts/lint.js --setup'
- name: 'Run ESLint'
run: 'node scripts/lint.js --eslint'
- name: 'Run actionlint'
run: 'node scripts/lint.js --actionlint'
- name: 'Run shellcheck'
run: 'node scripts/lint.js --shellcheck'
- name: 'Run yamllint'
run: 'node scripts/lint.js --yamllint'
- 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'
link_checker:
name: 'Link Checker'
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
- name: 'Link Checker'
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
with:
args: '--verbose ./**/*.md'
fail: true
test_linux:
name: 'Test (Linux)'
runs-on: 'ubuntu-latest'
runs-on: 'gemini-cli-ubuntu-16-core'
needs:
- 'lint'
- 'merge_queue_skipper'
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
permissions:
contents: 'read'
checks: 'write'
pull-requests: 'write'
strategy:
fail-fast: false
matrix:
node-version:
- '20.x'
@@ -37,7 +137,10 @@ jobs:
node-version: '${{ matrix.node-version }}'
cache: 'npm'
- name: 'Install dependencies'
- name: 'Build project'
run: 'npm run build'
- name: 'Install dependencies for testing'
run: 'npm ci'
- name: 'Run tests and generate reports'
@@ -45,6 +148,12 @@ jobs:
NO_COLOR: true
run: 'npm run test:ci'
- name: 'Bundle'
run: 'npm run bundle'
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
- name: 'Wait for file system sync'
run: 'sleep 2'
@@ -69,9 +178,16 @@ jobs:
test_mac:
name: 'Test (Mac)'
runs-on: '${{ matrix.os }}'
needs:
- 'lint'
- 'merge_queue_skipper'
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
permissions:
contents: 'read'
checks: 'write'
pull-requests: 'write'
continue-on-error: true
strategy:
fail-fast: false
matrix:
os:
- 'macos-latest'
@@ -89,7 +205,10 @@ jobs:
node-version: '${{ matrix.node-version }}'
cache: 'npm'
- name: 'Install dependencies'
- name: 'Build project'
run: 'npm run build'
- name: 'Install dependencies for testing'
run: 'npm ci'
- name: 'Run tests and generate reports'
@@ -97,6 +216,12 @@ jobs:
NO_COLOR: true
run: 'npm run test:ci -- --coverage.enabled=false'
- name: 'Bundle'
run: 'npm run bundle'
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
- name: 'Wait for file system sync'
run: 'sleep 2'
@@ -128,7 +253,13 @@ jobs:
codeql:
name: 'CodeQL'
runs-on: 'ubuntu-latest'
runs-on: 'gemini-cli-ubuntu-16-core'
needs: 'merge_queue_skipper'
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
permissions:
actions: 'read'
contents: 'read'
security-events: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
@@ -143,12 +274,37 @@ jobs:
- name: 'Perform CodeQL Analysis'
uses: 'github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/analyze@v3
# Check for changes in bundle size.
bundle_size:
name: 'Check Bundle Size'
needs: 'merge_queue_skipper'
if: "${{github.event_name == 'pull_request' && needs.merge_queue_skipper.outputs.skip == 'false'}}"
runs-on: 'gemini-cli-ubuntu-16-core'
permissions:
contents: 'read' # For checkout
pull-requests: 'write' # For commenting
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 1
- uses: 'preactjs/compressed-size-action@946a292cd35bd1088e0d7eb92b69d1a8d5b5d76a'
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './bundle/**/*.{js,sb}'
minimum-change-threshold: '1000'
compression: 'none'
clean-script: 'clean'
test_windows:
name: 'Slow Test - Win'
runs-on: 'windows-latest'
runs-on: 'gemini-cli-windows-16-core'
needs: 'merge_queue_skipper'
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
continue-on-error: true
strategy:
fail-fast: false
steps:
- name: 'Checkout'
@@ -184,6 +340,14 @@ jobs:
run: 'npm ci'
shell: 'pwsh'
- name: 'Build project'
run: 'npm run build'
shell: 'pwsh'
env:
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'production'
- name: 'Run tests and generate reports'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
@@ -194,20 +358,34 @@ jobs:
run: 'npm run test:ci -- --coverage.enabled=false'
shell: 'pwsh'
- name: 'Bundle'
run: 'npm run bundle'
shell: 'pwsh'
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
shell: 'pwsh'
ci:
name: 'CI'
if: 'always()'
needs:
- 'lint'
- 'link_checker'
- 'test_linux'
- 'test_mac'
- 'codeql'
runs-on: 'ubuntu-latest'
- 'bundle_size'
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
- name: 'Check all job results'
run: |
if [[ (${{ needs.test_linux.result }} != 'success' && ${{ needs.test_linux.result }} != 'skipped') || \
if [[ (${{ needs.lint.result }} != 'success' && ${{ needs.lint.result }} != 'skipped') || \
(${{ needs.link_checker.result }} != 'success' && ${{ needs.link_checker.result }} != 'skipped') || \
(${{ needs.test_linux.result }} != 'success' && ${{ needs.test_linux.result }} != 'skipped') || \
(${{ needs.test_mac.result }} != 'success' && ${{ needs.test_mac.result }} != 'skipped') || \
(${{ needs.codeql.result }} != 'success' && ${{ needs.codeql.result }} != 'skipped') ]]; then
(${{ needs.codeql.result }} != 'success' && ${{ needs.codeql.result }} != 'skipped') || \
(${{ needs.bundle_size.result }} != 'success' && ${{ needs.bundle_size.result }} != 'skipped') ]]; then
echo "One or more CI jobs failed."
exit 1
fi
+77 -67
View File
@@ -1,32 +1,56 @@
name: 'Testing: E2E'
on:
workflow_call:
push:
branches:
- 'main'
# This will run for PRs from the base repository, providing secrets.
pull_request:
branches:
- 'main'
- 'release/**'
# This will run for PRs from forks when a label is added.
pull_request_target:
types: ['labeled']
merge_group:
workflow_dispatch:
inputs:
version:
description: 'The version of the published package'
branch_ref:
description: 'Branch to run on'
required: true
default: 'main'
type: 'string'
cli-package-name:
description: 'The name of the CLI package to install'
required: true
type: 'string'
image-uri:
description: 'The URI of the built sandbox image.'
required: true
type: 'string'
secrets:
GEMINI_API_KEY:
required: true
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: |-
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
jobs:
merge_queue_skipper:
name: 'Merge Queue Skipper'
runs-on: 'gemini-cli-ubuntu-16-core'
permissions: 'read-all'
outputs:
skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
steps:
- id: 'merge-queue-e2e-skipper'
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
with:
secret: '${{ secrets.GITHUB_TOKEN }}'
continue-on-error: true
e2e_linux:
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
needs: 'merge_queue_skipper'
if: |
github.event_name == 'push' ||
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
(github.event_name == 'push' ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository ||
github.event.label.name == 'maintainer:e2e:ok'
runs-on: 'ubuntu-latest'
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok'))
runs-on: 'gemini-cli-ubuntu-16-core'
strategy:
fail-fast: false
matrix:
@@ -35,6 +59,7 @@ jobs:
- 'sandbox:docker'
node-version:
- '20.x'
steps:
- name: 'Checkout (fork)'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
@@ -49,34 +74,26 @@ jobs:
with:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Setup NPMRC'
uses: './.github/actions/setup-npmrc'
- name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
with:
registry: 'ghcr.io'
username: '${{ github.repository_owner }}'
password: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Pull sandbox image'
run: 'docker pull ${{ inputs.image-uri }}'
node-version: '${{ matrix.node-version }}'
- name: 'Install dependencies'
env:
CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
VERSION: '${{ inputs.version }}'
run: 'npm install ${{ env.CLI_PACKAGE_NAME }}@${{ env.VERSION }}'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Set up Docker'
if: "matrix.sandbox == 'sandbox:docker'"
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
KEEP_OUTPUT: 'true'
VERBOSE: 'true'
INTEGRATION_TEST_USE_INSTALLED_GEMINI: 'true'
GEMINI_SANDBOX_IMAGE: '${{ inputs.image-uri }}'
shell: 'bash'
run: |
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
@@ -87,12 +104,14 @@ jobs:
e2e_mac:
name: 'E2E Test (macOS)'
needs: 'merge_queue_skipper'
if: |
github.event_name == 'push' ||
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
(github.event_name == 'push' ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository ||
github.event.label.name == 'maintainer:e2e:ok'
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok'))
runs-on: 'macos-latest'
steps:
- name: 'Checkout (fork)'
@@ -113,16 +132,11 @@ jobs:
with:
node-version: '20.x'
- name: 'Setup NPMRC'
uses: './.github/actions/setup-npmrc'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Install dependencies'
env:
CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
VERSION: '${{ inputs.version }}'
run: 'npm install ${{ env.CLI_PACKAGE_NAME }}@${{ env.VERSION }}'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Fix rollup optional dependencies on macOS'
if: "runner.os == 'macOS'"
@@ -135,18 +149,19 @@ jobs:
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
INTEGRATION_TEST_USE_INSTALLED_GEMINI: 'true'
run: 'npm run test:integration:sandbox:none'
e2e_windows:
name: 'Slow E2E - Win'
needs: 'merge_queue_skipper'
if: |
github.event_name == 'push' ||
(needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip == 'false') &&
(github.event_name == 'push' ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository ||
github.event.label.name == 'maintainer:e2e:ok'
runs-on: 'windows-latest'
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok'))
runs-on: 'gemini-cli-windows-16-core'
continue-on-error: true
steps:
@@ -187,16 +202,12 @@ jobs:
npm config set registry https://registry.npmjs.org/
shell: 'pwsh'
- name: 'Setup NPMRC'
uses: './.github/actions/setup-npmrc'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Install dependencies'
env:
CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
VERSION: '${{ inputs.version }}'
run: 'npm install ${{ env.CLI_PACKAGE_NAME }}@${{ env.VERSION }}'
run: 'npm ci'
shell: 'pwsh'
- name: 'Build project'
run: 'npm run build'
shell: 'pwsh'
- name: 'Run E2E tests'
@@ -208,7 +219,6 @@ jobs:
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
INTEGRATION_TEST_USE_INSTALLED_GEMINI: 'true'
shell: 'pwsh'
run: 'npm run test:integration:sandbox:none'
@@ -219,13 +229,13 @@ jobs:
github.event_name == 'push' ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository ||
github.event.label.name == 'maintainer:e2e:ok'
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok')
)
needs:
- 'e2e_linux'
- 'e2e_mac'
runs-on: 'ubuntu-latest'
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
- name: 'Check E2E test results'
run: |
@@ -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'
-30
View File
@@ -1,30 +0,0 @@
# .github/workflows/lint.yml
name: 'Linting'
on:
workflow_call:
permissions:
contents: 'read'
jobs:
lint:
name: 'Lint All'
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 0
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci --only=dev'
- name: 'Run linting'
run: 'npm run lint:all'
-91
View File
@@ -1,91 +0,0 @@
# .github/workflows/pr-checks.yml
name: 'Checks'
on:
pull_request:
branches:
- 'main'
- 'release/**'
merge_group:
concurrency:
group: '${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}'
cancel-in-progress: true
env:
CLI_PACKAGE_NAME: '@google/gemini-cli'
permissions:
contents: 'read'
packages: 'write'
pull-requests: 'write'
security-events: 'write'
checks: 'write'
statuses: 'write'
actions: 'read'
attestations: 'read'
deployments: 'read'
discussions: 'read'
issues: 'read'
pages: 'read'
repository-projects: 'read'
id-token: 'write'
jobs:
lint:
name: 'Lint'
uses: './.github/workflows/lint.yml'
get-vars:
name: 'Get Environment Variables'
runs-on: 'ubuntu-latest'
environment: 'dev'
outputs:
cli-package-name: '${{ steps.get-vars.outputs.cli-package-name }}'
npm-registry-scope: '${{ steps.get-vars.outputs.npm-registry-scope }}'
npm-registry-url: '${{ steps.get-vars.outputs.npm-registry-url }}'
steps:
- id: 'get-vars'
env:
CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
NPM_REGISTRY_SCOPE: '${{ vars.NPM_REGISTRY_SCOPE }}'
NPM_REGISTRY_URL: '${{ vars.NPM_REGISTRY_URL }}'
run: |
echo "cli-package-name=${CLI_PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
echo "npm-registry-scope=${NPM_REGISTRY_SCOPE}" >> "$GITHUB_OUTPUT"
echo "npm-registry-url=${NPM_REGISTRY_URL}" >> "$GITHUB_OUTPUT"
build-and-publish:
name: 'Build and Publish CI Bundle'
needs: ['lint', 'get-vars']
uses: './.github/workflows/build-and-publish.yml'
with:
cli-package-name: '${{ needs.get-vars.outputs.cli-package-name }}'
npm-registry-scope: '${{ needs.get-vars.outputs.npm-registry-scope }}'
npm-registry-url: '${{ needs.get-vars.outputs.npm-registry-url }}'
ci:
name: 'CI Checks'
needs: 'lint'
uses: './.github/workflows/ci.yml'
e2e:
name: 'E2E Checks'
needs: ['build-and-publish', 'get-vars']
uses: './.github/workflows/e2e.yml'
permissions:
contents: 'read'
packages: 'read'
with:
version: '${{ needs.build-and-publish.outputs.version }}'
cli-package-name: '${{ needs.get-vars.outputs.cli-package-name }}'
image-uri: '${{ needs.build-and-publish.outputs.image-uri }}'
secrets:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
bundle-size:
name: 'Bundle Size Check'
needs: 'build-and-publish'
uses: './.github/workflows/bundle-size.yml'
with:
version: '${{ needs.build-and-publish.outputs.version }}'
@@ -1,144 +0,0 @@
name: 'Release: Manual (Bundled)'
on:
workflow_dispatch:
inputs:
version:
description: 'The version to release (e.g., v0.1.11). Must be a valid semver string with a "v" prefix.'
required: true
type: 'string'
ref:
description: 'The branch, tag, or SHA to release from.'
required: true
type: 'string'
npm_channel:
description: 'The npm channel to publish to'
required: true
type: 'choice'
options:
- 'dev'
- 'preview'
- 'nightly'
- 'latest'
default: 'latest'
dry_run:
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
required: true
type: 'boolean'
default: true
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
skip_github_release:
description: 'Select to skip creating a GitHub release (only used when environment is PROD)'
required: false
type: 'boolean'
default: false
environment:
description: 'Environment'
required: false
type: 'choice'
options:
- 'prod'
- 'dev'
default: 'prod'
jobs:
release:
runs-on: 'ubuntu-latest'
environment: "${{ github.event.inputs.environment || 'prod' }}"
permissions:
contents: 'write'
packages: 'write'
issues: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
fetch-depth: 0
- name: 'Checkout Release Code'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
ref: '${{ github.event.inputs.ref }}'
path: 'release'
fetch-depth: 0
- name: 'Debug Inputs'
shell: 'bash'
env:
JSON_INPUTS: '${{ toJSON(inputs) }}'
run: 'echo "$JSON_INPUTS"'
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version-file: './release/.nvmrc'
cache: 'npm'
- name: 'Install Dependencies'
working-directory: './release'
run: 'npm ci'
- name: 'Prepare Release Info'
id: 'release_info'
working-directory: './release'
env:
INPUT_VERSION: '${{ github.event.inputs.version }}'
run: |
RELEASE_VERSION="${INPUT_VERSION}"
echo "RELEASE_VERSION=${RELEASE_VERSION#v}" >> "${GITHUB_OUTPUT}"
echo "PREVIOUS_TAG=$(git describe --tags --abbrev=0)" >> "${GITHUB_OUTPUT}"
- name: 'Run Tests'
if: "${{github.event.inputs.force_skip_tests != 'true'}}"
uses: './.github/actions/run-tests'
with:
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
working-directory: './release'
- name: 'Publish Bundle'
uses: './.github/actions/publish-bundle'
with:
version: '${{ steps.release_info.outputs.RELEASE_VERSION }}'
tag: '${{ github.event.inputs.npm_channel }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
package-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
- name: '🔬 Verify Bundled NPM release by version'
if: '${{ github.event.inputs.dry_run == false && github.event.inputs.force_skip_tests == false }}'
working-directory: './release'
shell: 'bash'
env:
CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
RELEASE_VERSION: '${{ steps.release_info.outputs.RELEASE_VERSION }}'
run: |
npx -y "${CLI_PACKAGE_NAME}"@"${RELEASE_VERSION}" --version
- name: '🎉 Create GitHub Release'
working-directory: './release'
if: "${{ github.event.inputs.dry_run == false && github.event.inputs.skip_github_release == false && github.event.inputs.npm_channel != 'dev' && vars.NPM_REGISTRY_URL != 'https://npm.pkg.github.com/' }}"
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
RELEASE_TAG: '${{ github.event.inputs.version }}'
PREVIOUS_TAG: '${{ steps.release_info.outputs.PREVIOUS_TAG }}'
run: |
gh release create "${RELEASE_TAG}" \
bundle/gemini.js \
--title "Release ${RELEASE_TAG}" \
--notes-start-tag "${PREVIOUS_TAG}" \
--generate-notes
- name: 'Create Issue on Failure'
if: '${{ failure() && github.event.inputs.dry_run == false }}'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
RELEASE_TAG: '${{ github.event.inputs.version }}'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |
gh issue create \
--title 'Manual Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
--body 'The manual release workflow failed. See the full run for details: ${DETAILS_URL}' \
--label 'kind/bug,release-failure,priority/p0'
+2 -2
View File
@@ -1,4 +1,4 @@
name: 'Release: Manual (Depreciated)'
name: 'Release: Manual'
on:
workflow_dispatch:
@@ -100,7 +100,7 @@ jobs:
working-directory: './release'
- name: 'Publish Release'
uses: './.github/actions/publish-release-deprecated'
uses: './.github/actions/publish-release'
with:
force-skip-tests: '${{ github.event.inputs.force_skip_tests }}'
release-version: '${{ steps.release_info.outputs.RELEASE_VERSION }}'
@@ -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:
+7 -4
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"
@@ -227,7 +230,7 @@ jobs:
run: 'npm ci'
- name: 'Publish Release'
uses: './.github/actions/publish-release-deprecated'
uses: './.github/actions/publish-release'
with:
release-version: '${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
release-tag: 'v${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
@@ -293,7 +296,7 @@ jobs:
run: 'npm ci'
- name: 'Publish Release'
uses: './.github/actions/publish-release-deprecated'
uses: './.github/actions/publish-release'
with:
release-version: '${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
release-tag: 'v${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
+49
View File
@@ -0,0 +1,49 @@
name: 'Release Sandbox'
on:
workflow_dispatch:
inputs:
ref:
description: 'The branch, tag, or SHA to release from.'
required: false
type: 'string'
default: 'main'
dry-run:
description: 'Whether this is a dry run.'
required: false
type: 'boolean'
default: true
jobs:
build:
runs-on: 'ubuntu-latest'
permissions:
contents: 'read'
packages: 'write'
issues: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0
- name: 'Push'
uses: './.github/actions/push-sandbox'
with:
dockerhub-username: '${{ secrets.DOCKER_SERVICE_ACCOUNT_NAME }}'
dockerhub-token: '${{ secrets.DOCKER_SERVICE_ACCOUNT_KEY }}'
github-actor: '${{ github.actor }}'
github-secret: '${{ secrets.GITHUB_TOKEN }}'
github-sha: '${{ github.sha }}'
github-ref-name: '${{github.event.inputs.ref}}'
dry-run: '${{ github.event.inputs.dry-run }}'
- name: 'Create Issue on Failure'
if: '${{ failure() && github.event.inputs.dry-run == false }}'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |
gh issue create \
--title 'Sandbox Release Failed on $(date +'%Y-%m-%d')' \
--body 'The sandbox-release workflow failed. See the full run for details: ${DETAILS_URL}' \
--label 'kind/bug,release-failure,priority/p0'
-3
View File
@@ -17,7 +17,6 @@
# Dependency directory
node_modules
bower_components
package-lock.json
# Editors
.idea
@@ -55,6 +54,4 @@ gha-creds-*.json
# Log files
patch_output.log
# Local npm configuration
.npmrc
.genkit
+1
View File
@@ -0,0 +1 @@
@google:registry=https://wombat-dressing-room.appspot.com
+16 -39
View File
@@ -53,6 +53,21 @@ 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.
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.
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
If you're looking for an issue to work on, check out our list of issues that are
@@ -304,45 +319,7 @@ npm run lint
- **Imports:** Pay special attention to import paths. The project uses ESLint to
enforce restrictions on relative imports between packages.
### Setting Up Your NPM Environment
**Note:** The following instructions are intended for contributors who need to
work with pre-release versions hosted on the private GitHub Packages registry.
General contributors who only use the public `npmjs.org` registry may not need
to perform this setup.
This project uses a hybrid registry model:
- Production packages (e.g., `@google/gemini-cli`) are published to `npmjs.org`.
- Pre-release and development packages (e.g., `@google-gemini/gemini-cli`) are
published to **GitHub Packages**.
To work with both registries, you need to configure your user-level `~/.npmrc`
file to authenticate with GitHub Packages. We have a script to automate this.
#### Configure Your `~/.npmrc` File
Run the following command to automatically configure your `~/.npmrc` file:
```bash
npm run setup-npmrc
```
This command will:
1. Check that you have the [GitHub CLI (`gh`)](https://cli.github.com/)
installed and that you are logged in.
2. **Back up** your existing `~/.npmrc` to `~/.npmrc.bak` if a backup doesn't
already exist.
3. **Overwrite** your `~/.npmrc` with the correct configuration to pull
packages from both `npmjs.org` and `npm.pkg.github.com` based on their
scope.
After running this, you can install packages from both registries without any
further configuration. If your GitHub token expires, you can simply run the
command again to update it.
### Project Structure
### Project structure
- `packages/`: Contains the individual sub-packages of the project.
- `a2a-server`: A2A server implementation for the Gemini CLI. (Experimental)
+14 -34
View File
@@ -1,38 +1,11 @@
# --- Builder Stage ---
FROM docker.io/library/node:20-slim AS builder
ARG CLI_VERSION
ARG NPM_REGISTRY_SCOPE
ARG NPM_REGISTRY_URL
ARG CLI_PACKAGE_NAME
# Set up npm global package folder
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
# Ensure the node user has permissions to write to the global npm directory
RUN mkdir -p /usr/local/share/npm-global && chown -R node:node /usr/local/share/npm-global
# Configure npm to use GitHub Packages
RUN --mount=type=secret,id=GITHUB_TOKEN \
echo "${NPM_REGISTRY_SCOPE}:registry=${NPM_REGISTRY_URL}" > /home/node/.npmrc && \
echo "//npm.pkg.github.com/:_authToken=$(cat /run/secrets/GITHUB_TOKEN)" >> /home/node/.npmrc && \
chown -R node:node /home/node/.npmrc
# Switch to non-root user
USER node
# Install the Gemini CLI package
RUN npm install -g ${CLI_PACKAGE_NAME}@${CLI_VERSION} && \
npm cache clean --force
# --- Final Stage ---
FROM docker.io/library/node:20-slim
ARG SANDBOX_NAME="gemini-cli-sandbox"
ARG CLI_VERSION_ARG
ENV SANDBOX="$SANDBOX_NAME"
ENV CLI_VERSION=$CLI_VERSION_ARG
# Install runtime dependencies
# install minimal set of packages, then clean up
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
@@ -56,15 +29,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Set up npm global package folder and user
# set up npm global package folder under /usr/local/share
# give it to non-root user node, already set up in base image
RUN mkdir -p /usr/local/share/npm-global \
&& chown -R node:node /usr/local/share/npm-global
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
# switch to non-root user node
USER node
# Copy installed package from the builder stage
COPY --from=builder /usr/local/share/npm-global /usr/local/share/npm-global
# install gemini-cli and clean up
COPY packages/cli/dist/google-gemini-cli-*.tgz /tmp/gemini-cli.tgz
COPY packages/core/dist/google-gemini-cli-core-*.tgz /tmp/gemini-core.tgz
RUN npm install -g /tmp/gemini-cli.tgz /tmp/gemini-core.tgz \
&& npm cache clean --force \
&& rm -f /tmp/gemini-{cli,core}.tgz
# Default entrypoint
# default entrypoint when none specified
CMD ["gemini"]
+106
View File
@@ -3,6 +3,112 @@
Wondering what's new in Gemini CLI? This document provides key highlights and
notable changes to Gemini CLI.
## 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
+3
View File
@@ -121,6 +121,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).
+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:
+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 an image 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.
+44
View File
@@ -0,0 +1,44 @@
# 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:
- **Auto (recommended):** Let the system choose the best model for your task.
Typically, this is the best option.
- **Pro:** For complex tasks that require deep reasoning and creativity. The Pro
model may take longer to return a response.
- **Flash:** For tasks that need a balance of speed and reasoning. The Flash
model will usually return a faster response than Pro.
- **Flash-Lite:** For simple tasks that need to be done quickly. The Flash-Lite
model is typically the fastest.
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.
+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.
-15
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.
+245 -121
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,6 +94,8 @@ 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.preferredEditor`** (string):
@@ -96,11 +103,11 @@ their corresponding top-level category object in your `settings.json` file.
- **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 +115,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 +171,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 +193,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,8 +205,16 @@ 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):
@@ -190,32 +223,47 @@ their corresponding top-level category object in your `settings.json` file.
- **`ui.showCitations`** (boolean):
- **Description:** Show citations for generated text in the chat.
- **Default:** `true`
- **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 +272,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 +288,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.2`
- **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,"thinkingBudget":-1},"temperature":1,"topP":0.95,"topK":64}}},"gemini-2.5-pro":{"extends":"chat-base","modelConfig":{"model":"gemini-2.5-pro"}},"gemini-2.5-flash":{"extends":"chat-base","modelConfig":{"model":"gemini-2.5-flash"}},"gemini-2.5-flash-lite":{"extends":"chat-base","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 +337,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 +392,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 +432,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 +488,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 +545,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 +875,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 +895,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 +902,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 +914,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)
+2 -2
View File
@@ -50,6 +50,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 +60,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:
+8 -4
View File
@@ -72,6 +72,10 @@
"label": "Keyboard Shortcuts",
"slug": "docs/cli/keyboard-shortcuts"
},
{
"label": "Model Selection",
"slug": "docs/cli/model"
},
{
"label": "Sandbox",
"slug": "docs/cli/sandbox"
@@ -116,6 +120,10 @@
{
"label": "Memory Import Processor",
"slug": "docs/core/memport"
},
{
"label": "Policy Engine",
"slug": "docs/core/policy-engine"
}
]
},
@@ -130,10 +138,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');
+19 -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;
@@ -311,6 +323,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 +400,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 +1009,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 +1021,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();
-1
View File
@@ -8,7 +8,6 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['./vitest.setup.ts'],
testTimeout: 300000, // 5 minutes
globalSetup: './globalSetup.ts',
reporters: ['default'],
-58
View File
@@ -1,58 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
// Mock the ClearcutLogger module to prevent E2E tests from failing due to a missing generated file.
// The ClearcutLogger (which imports git-commit.js) is not needed for E2E test validation,
// as tests rely on the OpenTelemetry logger which writes to telemetry.log.
vi.mock(
(await import('node:path')).resolve(
process.cwd(),
'packages/core/src/telemetry/clearcut-logger/clearcut-logger.js',
),
() => ({
ClearcutLogger: {
getInstance: vi.fn(() => ({
logStartSessionEvent: vi.fn(),
logNewPromptEvent: vi.fn(),
logToolCallEvent: vi.fn(),
logToolOutputTruncatedEvent: vi.fn(),
logFileOperationEvent: vi.fn(),
logApiRequestEvent: vi.fn(),
logApiResponseEvent: vi.fn(),
logApiErrorEvent: vi.fn(),
logChatCompressionEvent: vi.fn(),
logFlashFallbackEvent: vi.fn(),
logRipgrepFallbackEvent: vi.fn(),
logLoopDetectedEvent: vi.fn(),
logLoopDetectionDisabledEvent: vi.fn(),
logNextSpeakerCheck: vi.fn(),
logSlashCommandEvent: vi.fn(),
logMalformedJsonResponseEvent: vi.fn(),
logIdeConnectionEvent: vi.fn(),
logConversationFinishedEvent: vi.fn(),
logKittySequenceOverflowEvent: vi.fn(),
logInvalidChunkEvent: vi.fn(),
logContentRetryEvent: vi.fn(),
logContentRetryFailureEvent: vi.fn(),
logModelRoutingEvent: vi.fn(),
logExtensionInstallEvent: vi.fn(),
logExtensionUninstallEvent: vi.fn(),
logExtensionUpdateEvent: vi.fn(),
logExtensionEnableEvent: vi.fn(),
logExtensionDisableEvent: vi.fn(),
logSmartEditStrategyEvent: vi.fn(),
logSmartEditCorrectionEvent: vi.fn(),
logAgentStartEvent: vi.fn(),
logAgentFinishEvent: vi.fn(),
logWebFetchFallbackAttemptEvent: vi.fn(),
logEndSessionEvent: vi.fn(),
})),
clearInstance: vi.fn(),
},
}),
);
+117 -617
View File
File diff suppressed because it is too large Load Diff
+23 -20
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.13.0-nightly.20251029.cca41edc",
"version": "0.16.0-preview.3",
"engines": {
"node": ">=20.0.0"
},
@@ -8,12 +8,13 @@
"workspaces": [
"packages/*"
],
"private": "true",
"repository": {
"type": "git",
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0-nightly.20251029.cca41edc"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.16.0-preview.3"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -26,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",
@@ -33,26 +38,18 @@
"build:packages": "npm run build --workspaces",
"build:sandbox": "node scripts/build_sandbox.js",
"bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"test": "npm run test --workspaces --if-present --parallel",
"test:ci": "npm run build && npm run test:ci --workspaces --if-present --parallel && npm run test:scripts",
"test": "npm run test --workspaces --if-present",
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
"test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman",
"test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests",
"test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
"test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
"test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests",
"lint": "node scripts/lint.js --eslint",
"lint:eslint": "node scripts/lint.js --eslint",
"lint:actionlint": "node scripts/lint.js --actionlint",
"lint:shellcheck": "node scripts/lint.js --shellcheck",
"lint:yamllint": "node scripts/lint.js --yamllint",
"lint:prettier": "node scripts/lint.js --prettier",
"lint:sensitive-keywords": "node scripts/lint.js --sensitive-keywords",
"lint:tsconfig": "node scripts/lint.js --tsconfig",
"lint:setup": "node scripts/lint.js --setup",
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests && eslint scripts",
"lint:fix": "eslint . --fix --ext .ts,.tsx && eslint integration-tests --fix && eslint scripts --fix && npm run format",
"lint:ci": "npm run lint:all",
"lint:all": "npm run lint:setup && npm-run-all --parallel lint:eslint lint:actionlint lint:shellcheck lint:yamllint lint:prettier lint:sensitive-keywords lint:tsconfig",
"lint:all": "node scripts/lint.js",
"format": "prettier --experimental-cli --write .",
"typecheck": "npm run typecheck --workspaces --if-present",
"preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
@@ -62,10 +59,10 @@
"telemetry": "node scripts/telemetry.js",
"check:lockfile": "node scripts/check-lockfile.js",
"clean": "node scripts/clean.js",
"pre-commit": "node scripts/pre-commit.js",
"setup-npmrc": "node scripts/configure-registry.js"
"pre-commit": "node scripts/pre-commit.js"
},
"overrides": {
"ink": "npm:@jrichman/ink@6.4.3",
"wrap-ansi": "9.0.2",
"cliui": {
"wrap-ansi": "7.0.0"
@@ -86,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",
@@ -102,6 +101,7 @@
"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",
@@ -110,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",
@@ -119,7 +119,7 @@
"yargs": "^17.7.2"
},
"dependencies": {
"@testing-library/dom": "^10.4.1",
"ink": "npm:@jrichman/ink@6.4.3",
"latest-version": "^9.0.0",
"simple-git": "^3.28.0"
},
@@ -135,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.13.0-nightly.20251029.cca41edc",
"version": "0.16.0-preview.3",
"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 -1
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,
@@ -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 });
+11 -12
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.13.0-nightly.20251029.cca41edc",
"version": "0.16.0-preview.3",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -17,7 +17,7 @@
"debug": "node --inspect-brk dist/index.js",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write .",
"test": "vitest run -- ",
"test": "vitest run",
"test:ci": "vitest run",
"typecheck": "tsc --noEmit"
},
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0-nightly.20251029.cca41edc"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.16.0-preview.3"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -42,7 +42,7 @@
"fzf": "^0.5.2",
"glob": "^10.4.5",
"highlight.js": "^11.11.1",
"ink": "^6.2.3",
"ink": "npm:@jrichman/ink@6.4.3",
"ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0",
"latest-version": "^9.0.0",
@@ -50,14 +50,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 +67,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: () => {
@@ -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,
});
@@ -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,
});
@@ -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);
+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));
@@ -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,
});
},
};
+27 -21
View File
@@ -21,27 +21,33 @@ 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),
debugLogger: {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
};
});
vi.mock('@modelcontextprotocol/sdk/client/index.js');
const mockedGetUserExtensionsDir =
+1 -5
View File
@@ -44,11 +44,7 @@ describe('validateAuthMethod', () => {
it('should return an error message if GEMINI_API_KEY is not set', () => {
vi.stubEnv('GEMINI_API_KEY', undefined);
expect(validateAuthMethod(AuthType.USE_GEMINI)).toBe(
'GEMINI_API_KEY not found. Find your existing key or generate a new one at: https://aistudio.google.com/apikey\n' +
'\n' +
'To continue, please set the GEMINI_API_KEY environment variable or add it to a .env file.',
);
expect(validateAuthMethod(AuthType.USE_GEMINI)).toBeNull();
});
});
-7
View File
@@ -17,13 +17,6 @@ export function validateAuthMethod(authMethod: string): string | null {
}
if (authMethod === AuthType.USE_GEMINI) {
if (!process.env['GEMINI_API_KEY']) {
return (
'GEMINI_API_KEY not found. Find your existing key or generate a new one at: https://aistudio.google.com/apikey\n' +
'\n' +
'To continue, please set the GEMINI_API_KEY environment variable or add it to a .env file.'
);
}
return null;
}
+60 -53
View File
@@ -36,7 +36,7 @@ vi.mock('./sandboxConfig.js', () => ({
vi.mock('fs', async (importOriginal) => {
const actualFs = await importOriginal<typeof import('fs')>();
const pathMod = await import('node:path');
const mockHome = '/mock/home/user';
const mockHome = pathMod.resolve(pathMod.sep, 'mock', 'home', 'user');
const MOCK_CWD1 = process.cwd();
const MOCK_CWD2 = pathMod.resolve(pathMod.sep, 'home', 'user', 'project');
@@ -69,7 +69,7 @@ vi.mock('os', async (importOriginal) => {
const actualOs = await importOriginal<typeof os>();
return {
...actualOs,
homedir: vi.fn(() => '/mock/home/user'),
homedir: vi.fn(() => path.resolve(path.sep, 'mock', 'home', 'user')),
};
});
@@ -451,6 +451,36 @@ describe('parseArguments', () => {
mockConsoleError.mockRestore();
});
it('should throw an error when resuming a session without prompt in non-interactive mode', async () => {
const originalIsTTY = process.stdin.isTTY;
process.stdin.isTTY = false;
process.argv = ['node', 'script.js', '--resume', 'session-id'];
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
try {
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect.stringContaining(
'When resuming a session, you must provide a message via --prompt (-p) or stdin',
),
);
} finally {
mockExit.mockRestore();
mockConsoleError.mockRestore();
process.stdin.isTTY = originalIsTTY;
}
});
it('should support comma-separated values for --allowed-tools', async () => {
process.argv = [
'node',
@@ -752,11 +782,11 @@ describe('mergeMcpServers', () => {
});
describe('mergeExcludeTools', () => {
const defaultExcludes = [
const defaultExcludes = new Set([
SHELL_TOOL_NAME,
EDIT_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
];
]);
const originalIsTTY = process.stdin.isTTY;
beforeEach(() => {
@@ -799,7 +829,7 @@ describe('mergeExcludeTools', () => {
argv,
);
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4', 'tool5']),
new Set(['tool1', 'tool2', 'tool3', 'tool4', 'tool5']),
);
expect(config.getExcludeTools()).toHaveLength(5);
});
@@ -821,7 +851,7 @@ describe('mergeExcludeTools', () => {
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2', 'tool3']),
new Set(['tool1', 'tool2', 'tool3']),
);
expect(config.getExcludeTools()).toHaveLength(3);
});
@@ -852,7 +882,7 @@ describe('mergeExcludeTools', () => {
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4']),
new Set(['tool1', 'tool2', 'tool3', 'tool4']),
);
expect(config.getExcludeTools()).toHaveLength(4);
});
@@ -863,7 +893,7 @@ describe('mergeExcludeTools', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getExcludeTools()).toEqual([]);
expect(config.getExcludeTools()).toEqual(new Set([]));
});
it('should return default excludes when no excludeTools are specified and it is not interactive', async () => {
@@ -881,9 +911,7 @@ describe('mergeExcludeTools', () => {
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2']),
);
expect(config.getExcludeTools()).toEqual(new Set(['tool1', 'tool2']));
expect(config.getExcludeTools()).toHaveLength(2);
});
@@ -903,9 +931,7 @@ describe('mergeExcludeTools', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getExcludeTools()).toEqual(
expect.arrayContaining(['tool1', 'tool2']),
);
expect(config.getExcludeTools()).toEqual(new Set(['tool1', 'tool2']));
expect(config.getExcludeTools()).toHaveLength(2);
});
@@ -1146,9 +1172,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(baseSettings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server1: { url: 'http://localhost:8080' },
});
expect(config.getAllowedMcpServers()).toEqual(['server1']);
});
it('should allow multiple specified MCP servers', async () => {
@@ -1162,10 +1186,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(baseSettings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server1: { url: 'http://localhost:8080' },
server3: { url: 'http://localhost:8082' },
});
expect(config.getAllowedMcpServers()).toEqual(['server1', 'server3']);
});
it('should handle server names that do not exist', async () => {
@@ -1179,16 +1200,14 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(baseSettings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server1: { url: 'http://localhost:8080' },
});
expect(config.getAllowedMcpServers()).toEqual(['server1', 'server4']);
});
it('should allow no MCP servers if the flag is provided but empty', async () => {
process.argv = ['node', 'script.js', '--allowed-mcp-server-names', ''];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(baseSettings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({});
expect(config.getAllowedMcpServers()).toEqual(['']);
});
it('should read allowMCPServers from settings', async () => {
@@ -1199,10 +1218,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
mcp: { allowed: ['server1', 'server2'] },
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server1: { url: 'http://localhost:8080' },
server2: { url: 'http://localhost:8081' },
});
expect(config.getAllowedMcpServers()).toEqual(['server1', 'server2']);
});
it('should read excludeMCPServers from settings', async () => {
@@ -1213,9 +1229,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
mcp: { excluded: ['server1', 'server2'] },
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server3: { url: 'http://localhost:8082' },
});
expect(config.getBlockedMcpServers()).toEqual(['server1', 'server2']);
});
it('should override allowMCPServers with excludeMCPServers if overlapping', async () => {
@@ -1229,9 +1243,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
},
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server2: { url: 'http://localhost:8081' },
});
expect(config.getAllowedMcpServers()).toEqual(['server1', 'server2']);
expect(config.getBlockedMcpServers()).toEqual(['server1']);
});
it('should prioritize mcp server flag if set', async () => {
@@ -1250,9 +1263,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
},
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server1: { url: 'http://localhost:8080' },
});
expect(config.getAllowedMcpServers()).toEqual(['server1']);
});
it('should prioritize CLI flag over both allowed and excluded settings', async () => {
@@ -1273,10 +1284,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
},
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server2: { url: 'http://localhost:8081' },
server3: { url: 'http://localhost:8082' },
});
expect(config.getAllowedMcpServers()).toEqual(['server2', 'server3']);
expect(config.getBlockedMcpServers()).toEqual([]);
});
});
@@ -1497,7 +1506,9 @@ describe('loadCliConfig folderTrust', () => {
describe('loadCliConfig with includeDirectories', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.mocked(os.homedir).mockReturnValue(
path.resolve(path.sep, 'mock', 'home', 'user'),
);
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
vi.spyOn(process, 'cwd').mockReturnValue(
path.resolve(path.sep, 'home', 'user', 'project'),
@@ -1545,7 +1556,7 @@ describe('loadCliConfig with includeDirectories', () => {
});
});
describe('loadCliConfig chatCompression', () => {
describe('loadCliConfig compressionThreshold', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
@@ -1558,28 +1569,24 @@ describe('loadCliConfig chatCompression', () => {
vi.restoreAllMocks();
});
it('should pass chatCompression settings to the core config', async () => {
it('should pass settings to the core config', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
model: {
chatCompression: {
contextPercentageThreshold: 0.5,
},
compressionThreshold: 0.5,
},
};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getChatCompression()).toEqual({
contextPercentageThreshold: 0.5,
});
expect(await config.getCompressionThreshold()).toBe(0.5);
});
it('should have undefined chatCompression if not in settings', async () => {
it('should have undefined compressionThreshold if not in settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {};
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getChatCompression()).toBeUndefined();
expect(await config.getCompressionThreshold()).toBeUndefined();
});
});
+57 -169
View File
@@ -4,23 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import process from 'node:process';
import { mcpCommand } from '../commands/mcp.js';
import type {
FileFilteringOptions,
MCPServerConfig,
OutputFormat,
GeminiCLIExtension,
} from '@google/gemini-cli-core';
import type { OutputFormat } from '@google/gemini-cli-core';
import { extensionsCommand } from '../commands/extensions.js';
import {
Config,
loadServerHierarchicalMemory,
setGeminiMdFilename as setServerGeminiMdFilename,
getCurrentGeminiMdFilename,
ApprovalMode,
@@ -38,6 +29,7 @@ import {
getPty,
EDIT_TOOL_NAME,
debugLogger,
loadServerHierarchicalMemory,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
@@ -49,9 +41,10 @@ import { appEvents } from '../utils/events.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { createPolicyEngineConfig } from './policy.js';
import { ExtensionManager } from './extension-manager.js';
import type { ExtensionLoader } from '@google/gemini-cli-core/src/utils/extensionLoader.js';
import type { ExtensionEvents } from '@google/gemini-cli-core/src/utils/extensionLoader.js';
import { requestConsentNonInteractive } from './extensions/consent.js';
import { promptForSetting } from './extensions/extensionSettings.js';
import type { EventEmitter } from 'node:stream';
export interface CliArgs {
query: string | undefined;
@@ -68,6 +61,9 @@ export interface CliArgs {
experimentalAcp: boolean | undefined;
extensions: string[] | undefined;
listExtensions: boolean | undefined;
resume: string | 'latest' | undefined;
listSessions: boolean | undefined;
deleteSession: string | undefined;
includeDirectories: string[] | undefined;
screenReader: boolean | undefined;
useSmartEdit: boolean | undefined;
@@ -179,6 +175,35 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
type: 'boolean',
description: 'List all available extensions and exit.',
})
.option('resume', {
alias: 'r',
type: 'string',
// `skipValidation` so that we can distinguish between it being passed with a value, without
// one, and not being passed at all.
skipValidation: true,
description:
'Resume a previous session. Use "latest" for most recent or index number (e.g. --resume 5)',
coerce: (value: string): string => {
// When --resume passed with a value (`gemini --resume 123`): value = "123" (string)
// When --resume passed without a value (`gemini --resume`): value = "" (string)
// When --resume not passed at all: this `coerce` function is not called at all, and
// `yargsInstance.argv.resume` is undefined.
if (value === '') {
return 'latest';
}
return value;
},
})
.option('list-sessions', {
type: 'boolean',
description:
'List available sessions for the current project and exit.',
})
.option('delete-session', {
type: 'string',
description:
'Delete a session by index number (use --list-sessions to see available sessions).',
})
.option('include-directories', {
type: 'array',
string: true,
@@ -234,6 +259,11 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
if (argv['prompt'] && argv['promptInteractive']) {
return 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together';
}
if (argv.resume && !argv.prompt && !process.stdin.isTTY) {
throw new Error(
'When resuming a session, you must provide a message via --prompt (-p) or stdin',
);
}
if (argv.yolo && argv['approvalMode']) {
return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.';
}
@@ -295,49 +325,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
return result as unknown as CliArgs;
}
// This function is now a thin wrapper around the server's implementation.
// It's kept in the CLI for now as App.tsx directly calls it for memory refresh.
// TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself.
export async function loadHierarchicalGeminiMemory(
currentWorkingDirectory: string,
includeDirectoriesToReadGemini: readonly string[] = [],
debugMode: boolean,
fileService: FileDiscoveryService,
settings: Settings,
extensionLoader: ExtensionLoader,
folderTrust: boolean,
memoryImportFormat: 'flat' | 'tree' = 'tree',
fileFilteringOptions?: FileFilteringOptions,
): Promise<{ memoryContent: string; fileCount: number; filePaths: string[] }> {
// FIX: Use real, canonical paths for a reliable comparison to handle symlinks.
const realCwd = fs.realpathSync(path.resolve(currentWorkingDirectory));
const realHome = fs.realpathSync(path.resolve(homedir()));
const isHomeDirectory = realCwd === realHome;
// If it is the home directory, pass an empty string to the core memory
// function to signal that it should skip the workspace search.
const effectiveCwd = isHomeDirectory ? '' : currentWorkingDirectory;
if (debugMode) {
debugLogger.debug(
`CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory} (memoryImportFormat: ${memoryImportFormat})`,
);
}
// Directly call the server function with the corrected path.
return loadServerHierarchicalMemory(
effectiveCwd,
includeDirectoriesToReadGemini,
debugMode,
fileService,
extensionLoader,
folderTrust,
memoryImportFormat,
fileFilteringOptions,
settings.context?.discoveryMaxDirs,
);
}
/**
* Creates a filter function to determine if a tool should be excluded.
*
@@ -429,26 +416,26 @@ export async function loadCliConfig(
requestSetting: promptForSetting,
workspaceDir: cwd,
enabledExtensionOverrides: argv.extensions,
eventEmitter: appEvents as EventEmitter<ExtensionEvents>,
});
await extensionManager.loadExtensions();
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
const { memoryContent, fileCount, filePaths } =
await loadHierarchicalGeminiMemory(
await loadServerHierarchicalMemory(
cwd,
settings.context?.loadMemoryFromIncludeDirectories
? includeDirectories
: [],
debugMode,
fileService,
settings,
extensionManager,
trustedFolder,
memoryImportFormat,
memoryFileFiltering,
settings.context?.discoveryMaxDirs,
);
let mcpServers = mergeMcpServers(settings, extensionManager.getExtensions());
const question = argv.promptInteractive || argv.prompt || '';
// Determine approval mode with backward compatibility
@@ -519,26 +506,8 @@ export async function loadCliConfig(
approvalMode,
);
// Debug: Log the merged policy configuration
// Only log when message bus integration is enabled (when policies are active)
const enableMessageBusIntegration =
settings.tools?.enableMessageBusIntegration ?? false;
if (enableMessageBusIntegration) {
debugLogger.debug('=== Policy Engine Configuration ===');
debugLogger.debug(
`Default decision: ${policyEngineConfig.defaultDecision}`,
);
debugLogger.debug(`Total rules: ${policyEngineConfig.rules?.length || 0}`);
if (policyEngineConfig.rules && policyEngineConfig.rules.length > 0) {
debugLogger.debug('Rules (sorted by priority):');
policyEngineConfig.rules.forEach((rule, index) => {
debugLogger.debug(
` [${index}] toolName: ${rule.toolName || '*'}, decision: ${rule.decision}, priority: ${rule.priority}, argsPattern: ${rule.argsPattern ? rule.argsPattern.source : 'none'}`,
);
});
}
debugLogger.debug('===================================');
}
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools);
@@ -583,37 +552,8 @@ export async function loadCliConfig(
const excludeTools = mergeExcludeTools(
settings,
extensionManager.getExtensions(),
extraExcludes.length > 0 ? extraExcludes : undefined,
);
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
if (!argv.allowedMcpServerNames) {
if (settings.mcp?.allowed) {
mcpServers = allowedMcpServers(
mcpServers,
settings.mcp.allowed,
blockedMcpServers,
);
}
if (settings.mcp?.excluded) {
const excludedNames = new Set(settings.mcp.excluded.filter(Boolean));
if (excludedNames.size > 0) {
mcpServers = Object.fromEntries(
Object.entries(mcpServers).filter(([key]) => !excludedNames.has(key)),
);
}
}
}
if (argv.allowedMcpServerNames) {
mcpServers = allowedMcpServers(
mcpServers,
argv.allowedMcpServerNames,
blockedMcpServers,
);
}
const useModelRouter = settings.experimental?.useModelRouter ?? true;
const defaultModel = useModelRouter
@@ -651,7 +591,11 @@ export async function loadCliConfig(
toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand,
mcpServerCommand: settings.mcp?.serverCommand,
mcpServers,
mcpServers: settings.mcpServers,
allowedMcpServers: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
blockedMcpServers: argv.allowedMcpServerNames
? [] // explicitly allowed servers overrides everything
: settings.mcp?.excluded,
userMemory: memoryContent,
geminiMdFileCount: fileCount,
geminiMdFilePaths: filePaths,
@@ -678,13 +622,15 @@ export async function loadCliConfig(
maxSessionTurns: settings.model?.maxSessionTurns ?? -1,
experimentalZedIntegration: argv.experimentalAcp || false,
listExtensions: argv.listExtensions || false,
listSessions: argv.listSessions || false,
deleteSession: argv.deleteSession,
enabledExtensions: argv.extensions,
extensionLoader: extensionManager,
blockedMcpServers,
enableExtensionReloading: settings.experimental?.extensionReloading,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
chatCompression: settings.model?.chatCompression,
compressionThreshold: settings.model?.compressionThreshold,
folderTrust,
interactive,
trustedFolder,
@@ -710,78 +656,20 @@ export async function loadCliConfig(
recordResponses: argv.recordResponses,
retryFetchErrors: settings.general?.retryFetchErrors ?? false,
ptyInfo: ptyInfo?.name,
modelConfigServiceConfig: settings.modelConfigs,
// TODO: loading of hooks based on workspace trust
enableHooks: settings.tools?.enableHooks ?? false,
hooks: settings.hooks || {},
});
}
function allowedMcpServers(
mcpServers: { [x: string]: MCPServerConfig },
allowMCPServers: string[],
blockedMcpServers: Array<{ name: string; extensionName: string }>,
) {
const allowedNames = new Set(allowMCPServers.filter(Boolean));
if (allowedNames.size > 0) {
mcpServers = Object.fromEntries(
Object.entries(mcpServers).filter(([key, server]) => {
const isAllowed = allowedNames.has(key);
if (!isAllowed) {
blockedMcpServers.push({
name: key,
extensionName: server.extension?.name || '',
});
}
return isAllowed;
}),
);
} else {
blockedMcpServers.push(
...Object.entries(mcpServers).map(([key, server]) => ({
name: key,
extensionName: server.extension?.name || '',
})),
);
mcpServers = {};
}
return mcpServers;
}
function mergeMcpServers(settings: Settings, extensions: GeminiCLIExtension[]) {
const mcpServers = { ...(settings.mcpServers || {}) };
for (const extension of extensions) {
if (!extension.isActive) {
continue;
}
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,
extension,
};
});
}
return mcpServers;
}
function mergeExcludeTools(
settings: Settings,
extensions: GeminiCLIExtension[],
extraExcludes?: string[] | undefined,
): string[] {
const allExcludeTools = new Set([
...(settings.tools?.exclude || []),
...(extraExcludes || []),
]);
for (const extension of extensions) {
if (!extension.isActive) {
continue;
}
for (const tool of extension.excludeTools || []) {
allExcludeTools.add(tool);
}
}
return [...allExcludeTools];
}
+99 -45
View File
@@ -12,7 +12,11 @@ import { ExtensionEnablementManager } from './extensions/extensionEnablement.js'
import { type Settings, SettingScope } from './settings.js';
import { createHash, randomUUID } from 'node:crypto';
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import {
isWorkspaceTrusted,
loadTrustedFolders,
TrustLevel,
} from './trustedFolders.js';
import {
cloneFromGit,
downloadFromGitHubRelease,
@@ -24,6 +28,7 @@ import {
ExtensionDisableEvent,
ExtensionEnableEvent,
ExtensionInstallEvent,
ExtensionLoader,
ExtensionUninstallEvent,
ExtensionUpdateEvent,
getErrorMessage,
@@ -32,6 +37,7 @@ import {
logExtensionInstallEvent,
logExtensionUninstall,
logExtensionUpdateEvent,
type ExtensionEvents,
type MCPServerConfig,
type ExtensionInstallMetadata,
type GeminiCLIExtension,
@@ -50,11 +56,7 @@ import {
maybePromptForSettings,
type ExtensionSetting,
} from './extensions/extensionSettings.js';
import type {
ExtensionEvents,
ExtensionLoader,
} from '@google/gemini-cli-core/src/utils/extensionLoader.js';
import { EventEmitter } from 'node:events';
import type { EventEmitter } from 'node:stream';
interface ExtensionManagerParams {
enabledExtensionOverrides?: string[];
@@ -62,6 +64,7 @@ interface ExtensionManagerParams {
requestConsent: (consent: string) => Promise<boolean>;
requestSetting: ((setting: ExtensionSetting) => Promise<string>) | null;
workspaceDir: string;
eventEmitter?: EventEmitter<ExtensionEvents>;
}
/**
@@ -69,7 +72,7 @@ interface ExtensionManagerParams {
*
* You must call `loadExtensions` prior to calling other methods on this class.
*/
export class ExtensionManager implements ExtensionLoader {
export class ExtensionManager extends ExtensionLoader {
private extensionEnablementManager: ExtensionEnablementManager;
private settings: Settings;
private requestConsent: (consent: string) => Promise<boolean>;
@@ -79,9 +82,9 @@ export class ExtensionManager implements ExtensionLoader {
private telemetryConfig: Config;
private workspaceDir: string;
private loadedExtensions: GeminiCLIExtension[] | undefined;
private eventEmitter: EventEmitter<ExtensionEvents>;
constructor(options: ExtensionManagerParams) {
super(options.eventEmitter);
this.workspaceDir = options.workspaceDir;
this.extensionEnablementManager = new ExtensionEnablementManager(
options.enabledExtensionOverrides,
@@ -98,7 +101,6 @@ export class ExtensionManager implements ExtensionLoader {
});
this.requestConsent = options.requestConsent;
this.requestSetting = options.requestSetting ?? undefined;
this.eventEmitter = new EventEmitter();
}
setRequestConsent(
@@ -122,25 +124,38 @@ export class ExtensionManager implements ExtensionLoader {
return this.loadedExtensions!;
}
extensionEvents(): EventEmitter<ExtensionEvents> {
return this.eventEmitter;
}
async installOrUpdateExtension(
installMetadata: ExtensionInstallMetadata,
previousExtensionConfig?: ExtensionConfig,
): Promise<GeminiCLIExtension> {
if (
(installMetadata.type === 'git' ||
installMetadata.type === 'github-release') &&
this.settings.security?.blockGitExtensions
) {
throw new Error(
'Installing extensions from remote sources is disallowed by your current settings.',
);
}
const isUpdate = !!previousExtensionConfig;
let newExtensionConfig: ExtensionConfig | null = null;
let localSourcePath: string | undefined;
let extension: GeminiCLIExtension | null;
try {
if (!isWorkspaceTrusted(this.settings).isTrusted) {
throw new Error(
`Could not install extension from untrusted folder at ${installMetadata.source}`,
);
if (
await this.requestConsent(
`The current workspace at "${this.workspaceDir}" is not trusted. Do you want to trust this workspace to install extensions?`,
)
) {
const trustedFolders = loadTrustedFolders();
trustedFolders.setValue(this.workspaceDir, TrustLevel.TRUST_FOLDER);
} else {
throw new Error(
`Could not install extension because the current workspace at ${this.workspaceDir} is not trusted.`,
);
}
}
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
await fs.promises.mkdir(extensionsDir, { recursive: true });
@@ -291,13 +306,13 @@ export class ExtensionManager implements ExtensionLoader {
await fs.promises.writeFile(metadataPath, metadataString);
// TODO: Gracefully handle this call failing, we should back up the old
// extension prior to overwriting it and then restore it.
// extension prior to overwriting it and then restore and restart it.
extension = await this.loadExtension(destinationPath)!;
if (!extension) {
throw new Error(`Extension not found`);
}
if (isUpdate) {
logExtensionUpdateEvent(
await logExtensionUpdateEvent(
this.telemetryConfig,
new ExtensionUpdateEvent(
hashValue(newExtensionConfig.name),
@@ -308,9 +323,8 @@ export class ExtensionManager implements ExtensionLoader {
'success',
),
);
this.eventEmitter.emit('extensionUpdated', { extension });
} else {
logExtensionInstallEvent(
await logExtensionInstallEvent(
this.telemetryConfig,
new ExtensionInstallEvent(
hashValue(newExtensionConfig.name),
@@ -320,7 +334,6 @@ export class ExtensionManager implements ExtensionLoader {
'success',
),
);
this.eventEmitter.emit('extensionInstalled', { extension });
this.enableExtension(newExtensionConfig.name, SettingScope.User);
}
} finally {
@@ -344,7 +357,7 @@ export class ExtensionManager implements ExtensionLoader {
? getExtensionId(config, installMetadata)
: undefined;
if (isUpdate) {
logExtensionUpdateEvent(
await logExtensionUpdateEvent(
this.telemetryConfig,
new ExtensionUpdateEvent(
hashValue(config?.name ?? ''),
@@ -356,7 +369,7 @@ export class ExtensionManager implements ExtensionLoader {
),
);
} else {
logExtensionInstallEvent(
await logExtensionInstallEvent(
this.telemetryConfig,
new ExtensionInstallEvent(
hashValue(newExtensionConfig?.name ?? ''),
@@ -385,8 +398,12 @@ export class ExtensionManager implements ExtensionLoader {
if (!extension) {
throw new Error(`Extension not found.`);
}
this.unloadExtension(extension);
const storage = new ExtensionStorage(extension.name);
await this.unloadExtension(extension);
const storage = new ExtensionStorage(
extension.installMetadata?.type === 'link'
? extension.name
: path.basename(extension.path),
);
await fs.promises.rm(storage.getExtensionDir(), {
recursive: true,
@@ -399,7 +416,7 @@ export class ExtensionManager implements ExtensionLoader {
this.extensionEnablementManager.remove(extension.name);
logExtensionUninstall(
await logExtensionUninstall(
this.telemetryConfig,
new ExtensionUninstallEvent(
hashValue(extension.name),
@@ -407,9 +424,11 @@ export class ExtensionManager implements ExtensionLoader {
'success',
),
);
this.eventEmitter.emit('extensionUninstalled', { extension });
}
/**
* Loads all installed extensions, should only be called once.
*/
async loadExtensions(): Promise<GeminiCLIExtension[]> {
if (this.loadedExtensions) {
throw new Error('Extensions already loaded, only load extensions once.');
@@ -421,12 +440,14 @@ export class ExtensionManager implements ExtensionLoader {
}
for (const subdir of fs.readdirSync(extensionsDir)) {
const extensionDir = path.join(extensionsDir, subdir);
await this.loadExtension(extensionDir);
}
return this.loadedExtensions;
}
/**
* Adds `extension` to the list of extensions and starts it if appropriate.
*/
private async loadExtension(
extensionDir: string,
): Promise<GeminiCLIExtension | null> {
@@ -437,6 +458,13 @@ export class ExtensionManager implements ExtensionLoader {
const installMetadata = loadInstallMetadata(extensionDir);
let effectiveExtensionPath = extensionDir;
if (
(installMetadata?.type === 'git' ||
installMetadata?.type === 'github-release') &&
this.settings.security?.blockGitExtensions
) {
return null;
}
if (installMetadata?.type === 'link') {
effectiveExtensionPath = installMetadata.source;
@@ -487,8 +515,9 @@ export class ExtensionManager implements ExtensionLoader {
),
id: getExtensionId(config, installMetadata),
};
this.eventEmitter.emit('extensionLoaded', { extension });
this.getExtensions().push(extension);
this.loadedExtensions = [...this.loadedExtensions, extension];
await this.maybeStartExtension(extension);
return extension;
} catch (e) {
debugLogger.error(
@@ -500,11 +529,17 @@ export class ExtensionManager implements ExtensionLoader {
}
}
private unloadExtension(extension: GeminiCLIExtension) {
/**
* Removes `extension` from the list of extensions and stops it if
* appropriate.
*/
private unloadExtension(
extension: GeminiCLIExtension,
): Promise<void> | undefined {
this.loadedExtensions = this.getExtensions().filter(
(entry) => extension !== entry,
);
this.eventEmitter.emit('extensionUnloaded', { extension });
return this.maybeStopExtension(extension);
}
loadExtensionConfig(extensionDir: string): ExtensionConfig {
@@ -554,6 +589,8 @@ export class ExtensionManager implements ExtensionLoader {
const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗');
let output = `${status} ${extension.name} (${extension.version})`;
output += `\n ID: ${extension.id}`;
output += `\n name: ${hashValue(extension.name)}`;
output += `\n Path: ${extension.path}`;
if (extension.installMetadata) {
output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`;
@@ -601,17 +638,27 @@ export class ExtensionManager implements ExtensionLoader {
throw new Error(`Extension with name ${name} does not exist.`);
}
const scopePath =
scope === SettingScope.Workspace ? this.workspaceDir : os.homedir();
this.extensionEnablementManager.disable(name, true, scopePath);
logExtensionDisable(
if (scope !== SettingScope.Session) {
const scopePath =
scope === SettingScope.Workspace ? this.workspaceDir : os.homedir();
this.extensionEnablementManager.disable(name, true, scopePath);
}
await logExtensionDisable(
this.telemetryConfig,
new ExtensionDisableEvent(hashValue(name), extension.id, scope),
);
extension.isActive = false;
this.eventEmitter.emit('extensionDisabled', { extension });
if (!this.config || this.config.getEnableExtensionReloading()) {
// Only toggle the isActive state if we are actually going to disable it
// in the current session, or we haven't been initialized yet.
extension.isActive = false;
}
await this.maybeStopExtension(extension);
}
/**
* Enables an existing extension for a given scope, and starts it if
* appropriate.
*/
async enableExtension(name: string, scope: SettingScope) {
if (
scope === SettingScope.System ||
@@ -625,15 +672,22 @@ export class ExtensionManager implements ExtensionLoader {
if (!extension) {
throw new Error(`Extension with name ${name} does not exist.`);
}
const scopePath =
scope === SettingScope.Workspace ? this.workspaceDir : os.homedir();
this.extensionEnablementManager.enable(name, true, scopePath);
logExtensionEnable(
if (scope !== SettingScope.Session) {
const scopePath =
scope === SettingScope.Workspace ? this.workspaceDir : os.homedir();
this.extensionEnablementManager.enable(name, true, scopePath);
}
await logExtensionEnable(
this.telemetryConfig,
new ExtensionEnableEvent(hashValue(name), extension.id, scope),
);
extension.isActive = true;
this.eventEmitter.emit('extensionEnabled', { extension });
if (!this.config || this.config.getEnableExtensionReloading()) {
// Only toggle the isActive state if we are actually going to disable it
// in the current session, or we haven't been initialized yet.
extension.isActive = true;
}
await this.maybeStartExtension(extension);
}
}
+187 -10
View File
@@ -16,7 +16,10 @@ import {
KeychainTokenStorage,
} from '@google/gemini-cli-core';
import { loadSettings, SettingScope } from './settings.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import {
isWorkspaceTrusted,
resetTrustedFoldersForTesting,
} from './trustedFolders.js';
import { createExtension } from '../test-utils/createExtension.js';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
import { join } from 'node:path';
@@ -182,6 +185,7 @@ describe('extension tests', () => {
requestSetting: mockPromptForSettings,
settings: loadSettings(tempWorkspaceDir).merged,
});
resetTrustedFoldersForTesting();
});
afterEach(() => {
@@ -590,6 +594,34 @@ describe('extension tests', () => {
consoleSpy.mockRestore();
});
it('should not load github extensions if blockGitExtensions is set', async () => {
createExtension({
extensionsDir: userExtensionsDir,
name: 'my-ext',
version: '1.0.0',
installMetadata: {
type: 'git',
source: 'http://somehost.com/foo/bar',
},
});
const blockGitExtensionsSetting = {
security: {
blockGitExtensions: true,
},
};
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: blockGitExtensionsSetting,
});
const extensions = await extensionManager.loadExtensions();
const extension = extensions.find((e) => e.name === 'my-ext');
expect(extension).toBeUndefined();
});
describe('id generation', () => {
it('should generate id from source for non-github git urls', async () => {
createExtension({
@@ -872,6 +904,111 @@ describe('extension tests', () => {
fs.rmSync(targetExtDir, { recursive: true, force: true });
});
it('should not install a github extension if blockGitExtensions is set', async () => {
const gitUrl = 'https://somehost.com/somerepo.git';
const blockGitExtensionsSetting = {
security: {
blockGitExtensions: true,
},
};
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: blockGitExtensionsSetting,
});
await extensionManager.loadExtensions();
await expect(
extensionManager.installOrUpdateExtension({
source: gitUrl,
type: 'git',
}),
).rejects.toThrow(
'Installing extensions from remote sources is disallowed by your current settings.',
);
});
it('should prompt for trust if workspace is not trusted', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: undefined,
});
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-local-extension',
version: '1.0.0',
});
await extensionManager.loadExtensions();
await extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
});
expect(mockRequestConsent).toHaveBeenCalledWith(
`The current workspace at "${tempWorkspaceDir}" is not trusted. Do you want to trust this workspace to install extensions?`,
);
});
it('should not install if user denies trust', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: undefined,
});
mockRequestConsent.mockImplementation(async (message) => {
if (
message.includes(
'is not trusted. Do you want to trust this workspace to install extensions?',
)
) {
return false;
}
return true;
});
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-local-extension',
version: '1.0.0',
});
await extensionManager.loadExtensions();
await expect(
extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
}),
).rejects.toThrow(
`Could not install extension because the current workspace at ${tempWorkspaceDir} is not trusted.`,
);
});
it('should add the workspace to trusted folders if user consents', async () => {
const trustedFoldersPath = path.join(
tempHomeDir,
'.gemini',
'trustedFolders.json',
);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: undefined,
});
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-local-extension',
version: '1.0.0',
});
await extensionManager.loadExtensions();
await extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
});
expect(fs.existsSync(trustedFoldersPath)).toBe(true);
const trustedFolders = JSON.parse(
fs.readFileSync(trustedFoldersPath, 'utf-8'),
);
expect(trustedFolders[tempWorkspaceDir]).toBe('TRUST_FOLDER');
});
describe.each([true, false])(
'with previous extension config: %s',
(isUpdate: boolean) => {
@@ -1470,6 +1607,34 @@ This extension will run the following MCP servers:
expect(fs.existsSync(otherExtDir)).toBe(true);
});
it('should uninstall an extension on non-matching extension directory name', async () => {
// Create an extension with a name that differs from the directory name.
const sourceExtDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'My-Local-Extension',
version: '1.0.0',
});
const newSourceExtDir = path.join(
userExtensionsDir,
'my-local-extension',
);
fs.renameSync(sourceExtDir, newSourceExtDir);
const otherExtDir = createExtension({
extensionsDir: userExtensionsDir,
name: 'other-extension',
version: '1.0.0',
});
await extensionManager.loadExtensions();
await extensionManager.uninstallExtension('my-local-extension', false);
expect(fs.existsSync(sourceExtDir)).toBe(false);
expect(fs.existsSync(newSourceExtDir)).toBe(false);
expect(extensionManager.getExtensions()).toHaveLength(1);
expect(fs.existsSync(otherExtDir)).toBe(true);
});
it('should throw an error if the extension does not exist', async () => {
await extensionManager.loadExtensions();
await expect(
@@ -1581,7 +1746,10 @@ This extension will run the following MCP servers:
});
await extensionManager.loadExtensions();
extensionManager.disableExtension('my-extension', SettingScope.User);
await extensionManager.disableExtension(
'my-extension',
SettingScope.User,
);
expect(
isEnabled({
name: 'my-extension',
@@ -1598,7 +1766,10 @@ This extension will run the following MCP servers:
});
await extensionManager.loadExtensions();
extensionManager.disableExtension('my-extension', SettingScope.Workspace);
await extensionManager.disableExtension(
'my-extension',
SettingScope.Workspace,
);
expect(
isEnabled({
name: 'my-extension',
@@ -1621,8 +1792,14 @@ This extension will run the following MCP servers:
});
await extensionManager.loadExtensions();
extensionManager.disableExtension('my-extension', SettingScope.User);
extensionManager.disableExtension('my-extension', SettingScope.User);
await extensionManager.disableExtension(
'my-extension',
SettingScope.User,
);
await extensionManager.disableExtension(
'my-extension',
SettingScope.User,
);
expect(
isEnabled({
name: 'my-extension',
@@ -1653,7 +1830,7 @@ This extension will run the following MCP servers:
});
await extensionManager.loadExtensions();
extensionManager.disableExtension('ext1', SettingScope.Workspace);
await extensionManager.disableExtension('ext1', SettingScope.Workspace);
expect(mockLogExtensionDisable).toHaveBeenCalled();
expect(ExtensionDisableEvent).toHaveBeenCalledWith(
@@ -1681,7 +1858,7 @@ This extension will run the following MCP servers:
version: '1.0.0',
});
await extensionManager.loadExtensions();
extensionManager.disableExtension('ext1', SettingScope.User);
await extensionManager.disableExtension('ext1', SettingScope.User);
let activeExtensions = getActiveExtensions();
expect(activeExtensions).toHaveLength(0);
@@ -1698,7 +1875,7 @@ This extension will run the following MCP servers:
version: '1.0.0',
});
await extensionManager.loadExtensions();
extensionManager.disableExtension('ext1', SettingScope.Workspace);
await extensionManager.disableExtension('ext1', SettingScope.Workspace);
let activeExtensions = getActiveExtensions();
expect(activeExtensions).toHaveLength(0);
@@ -1719,8 +1896,8 @@ This extension will run the following MCP servers:
},
});
await extensionManager.loadExtensions();
extensionManager.disableExtension('ext1', SettingScope.Workspace);
extensionManager.enableExtension('ext1', SettingScope.Workspace);
await extensionManager.disableExtension('ext1', SettingScope.Workspace);
await extensionManager.enableExtension('ext1', SettingScope.Workspace);
expect(mockLogExtensionEnable).toHaveBeenCalled();
expect(ExtensionEnableEvent).toHaveBeenCalledWith(
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as path from 'node:path';
import * as os from 'node:os';
import {
@@ -296,60 +296,52 @@ describe('extensionSettings', () => {
});
describe('promptForSetting', () => {
it('should use prompts with type "password" for sensitive settings', async () => {
const setting: ExtensionSetting = {
name: 'API Key',
description: 'Your secret key',
envVar: 'API_KEY',
sensitive: true,
};
vi.mocked(prompts).mockResolvedValue({ value: 'secret-key' });
it.each([
{
description:
'should use prompts with type "password" for sensitive settings',
setting: {
name: 'API Key',
description: 'Your secret key',
envVar: 'API_KEY',
sensitive: true,
},
expectedType: 'password',
promptValue: 'secret-key',
},
{
description:
'should use prompts with type "text" for non-sensitive settings',
setting: {
name: 'Username',
description: 'Your public username',
envVar: 'USERNAME',
sensitive: false,
},
expectedType: 'text',
promptValue: 'test-user',
},
{
description: 'should default to "text" if sensitive is undefined',
setting: {
name: 'Username',
description: 'Your public username',
envVar: 'USERNAME',
},
expectedType: 'text',
promptValue: 'test-user',
},
])('$description', async ({ setting, expectedType, promptValue }) => {
vi.mocked(prompts).mockResolvedValue({ value: promptValue });
const result = await promptForSetting(setting);
const result = await promptForSetting(setting as ExtensionSetting);
expect(prompts).toHaveBeenCalledWith({
type: 'password',
type: expectedType,
name: 'value',
message: 'API Key\nYour secret key',
message: `${setting.name}\n${setting.description}`,
});
expect(result).toBe('secret-key');
});
it('should use prompts with type "text" for non-sensitive settings', async () => {
const setting: ExtensionSetting = {
name: 'Username',
description: 'Your public username',
envVar: 'USERNAME',
// sensitive: false,
};
vi.mocked(prompts).mockResolvedValue({ value: 'test-user' });
const result = await promptForSetting(setting);
expect(prompts).toHaveBeenCalledWith({
type: 'text',
name: 'value',
message: 'Username\nYour public username',
});
expect(result).toBe('test-user');
});
it('should default to "text" if sensitive is undefined', async () => {
const setting: ExtensionSetting = {
name: 'Username',
description: 'Your public username',
envVar: 'USERNAME',
};
vi.mocked(prompts).mockResolvedValue({ value: 'test-user' });
const result = await promptForSetting(setting);
expect(prompts).toHaveBeenCalledWith({
type: 'text',
name: 'value',
message: 'Username\nYour public username',
});
expect(result).toBe('test-user');
expect(result).toBe(promptValue);
});
});
});

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