Compare commits
909 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52afcb3a12 | |||
| 1d32313a30 | |||
| 4acb870959 | |||
| 3283f55e7e | |||
| ddb32a3614 | |||
| 7c8a1da8fe | |||
| 7c4af82da4 | |||
| 71f1dcf39a | |||
| fefe97a1db | |||
| 639f8e70d2 | |||
| ef54e4ffbc | |||
| 4e69ba3bbe | |||
| 4d9e258a1e | |||
| 787c319e87 | |||
| fbbb6f2611 | |||
| 05b1c8101f | |||
| b179424161 | |||
| 6c67618624 | |||
| ea63a8401e | |||
| 7a419282c8 | |||
| 0c9b138f5e | |||
| 2f1fc3f359 | |||
| 4059a3e8ee | |||
| 2e5e9d736b | |||
| 04518b52c0 | |||
| c48fcaa8c3 | |||
| 1e5689e054 | |||
| c1486c47ee | |||
| 619da70070 | |||
| 0125547215 | |||
| b0bc7c3d99 | |||
| 10a83a6395 | |||
| 0ebac0b896 | |||
| 43203926b8 | |||
| 8e7fa7e233 | |||
| 3621ea0cb6 | |||
| b49d55584e | |||
| 8bc3b415c9 | |||
| b96fbd913e | |||
| cc89830b2a | |||
| c7a422ccdd | |||
| fbc79c34c9 | |||
| 589a7b59c6 | |||
| c4c444d378 | |||
| 4662b058e8 | |||
| 332512853e | |||
| 30d1662128 | |||
| 3453b977b8 | |||
| 5b2cea8eda | |||
| cd03d96b34 | |||
| 9b34762d97 | |||
| 0da4fd9d11 | |||
| 3e49206935 | |||
| 63fbc8ce18 | |||
| 7f189f4d5f | |||
| f3c1cbbabf | |||
| 443465a805 | |||
| 87053d9317 | |||
| e12c60fcab | |||
| e7c4801c79 | |||
| 7a99f0b5a2 | |||
| 708ba8902a | |||
| e59c872b3d | |||
| c3971754bf | |||
| bc3fa71234 | |||
| 137baa0a07 | |||
| b3e26de862 | |||
| 98093e604a | |||
| a7e45d47cd | |||
| 11f524c125 | |||
| 42329e0258 | |||
| bf62c3b21a | |||
| df938d6ee8 | |||
| a600588c20 | |||
| 7e6fb7e874 | |||
| 6af7a5c589 | |||
| dd679a6cdb | |||
| 5d4f4f421c | |||
| 742caa5dd8 | |||
| cc7459e403 | |||
| 40fbb61a1b | |||
| 197704c630 | |||
| f00b9f2727 | |||
| ffc48b16d4 | |||
| a6c16ab08d | |||
| bedff2ca79 | |||
| 7f06ad40c5 | |||
| e772dc2b85 | |||
| 5c8e49a0e3 | |||
| 4463671284 | |||
| 101b6fe767 | |||
| 714421c2da | |||
| 4421ef126f | |||
| b67806ae9a | |||
| 6d772a30c0 | |||
| 6959663646 | |||
| 123ad20e9b | |||
| b3d89a1075 | |||
| da09431be9 | |||
| 7352cb403c | |||
| 2b799cbbf0 | |||
| 12c33c1fe6 | |||
| ab932ffaa5 | |||
| e717c51aa1 | |||
| 53753f0455 | |||
| 32dd298351 | |||
| e30e650a77 | |||
| e544b940f1 | |||
| d0b77d9fa0 | |||
| 2c6aae863a | |||
| 93909a2dd3 | |||
| 3bcb3c3666 | |||
| 4873fce791 | |||
| e6d5477168 | |||
| 643bdf31d5 | |||
| d5c6bb9740 | |||
| 1452bb4ca4 | |||
| defb0fac2c | |||
| 9954779739 | |||
| 31b28ade01 | |||
| 8eb505fbba | |||
| 209381f06f | |||
| a2fe3d2ad0 | |||
| bb67d31739 | |||
| 54f0d9d0e5 | |||
| 1fa41af918 | |||
| 491e367f7c | |||
| 1cefe21d2a | |||
| 084b58a50e | |||
| 1f63f3331f | |||
| daceb9963f | |||
| 8e804c9fa1 | |||
| 9d04e04bc0 | |||
| 2a1ad1f5d9 | |||
| 34e0d9c0b6 | |||
| c886f08525 | |||
| d25459d815 | |||
| 1fcbdef994 | |||
| ff478781ad | |||
| 7bcc60e996 | |||
| f8a31f29aa | |||
| 95e4a60a83 | |||
| b20c8389f3 | |||
| 8bb6eca915 | |||
| 28e656f882 | |||
| 1c7774e35b | |||
| 431ee839a0 | |||
| 9a11567f73 | |||
| 181abde2ff | |||
| 3c3da655b0 | |||
| dc378e8d60 | |||
| b7daa7c702 | |||
| 61d0cc39fd | |||
| 6723c72fa5 | |||
| f8863f4d00 | |||
| 32da693b91 | |||
| 6bb3f27f6c | |||
| e1d8a356b0 | |||
| ad2e47dc20 | |||
| a9e56ee460 | |||
| af247a6cbd | |||
| 47ce39c46f | |||
| f2ab6d08c4 | |||
| 9072a4e5ee | |||
| 041e7c177c | |||
| 6fc7028031 | |||
| 89f682f081 | |||
| 1ef68e0612 | |||
| dd53e5c96a | |||
| e02a035ab4 | |||
| fb67ee5445 | |||
| 2dc79b3bd0 | |||
| 6ecdecbdcc | |||
| 122678cc09 | |||
| 7a72d255d8 | |||
| 4160d904da | |||
| 24c61147b8 | |||
| c0580eaf4b | |||
| 5586ad5f8a | |||
| e0f4f428fc | |||
| f75c48323c | |||
| 03bc1f3141 | |||
| 9237e95f11 | |||
| d96af8bacd | |||
| 9c5b5ff823 | |||
| 1d7090b8ac | |||
| e2d689ff2f | |||
| 7ba2b13870 | |||
| 9d992b32e4 | |||
| 00c4527a1b | |||
| 5bab5a7378 | |||
| 3f5ac384cc | |||
| d1e23b7c71 | |||
| 95fdc66e7d | |||
| fb6e2927f7 | |||
| 3372fd8df8 | |||
| 97e08fc804 | |||
| e92b7dfd74 | |||
| b92fa78a1e | |||
| 8e0d5076d6 | |||
| d6b6d5976d | |||
| 36f58a34b4 | |||
| d79dafc577 | |||
| 4e84431df3 | |||
| 916cfee08d | |||
| e73d4752df | |||
| fa27bc832f | |||
| f0f7a30d9f | |||
| 6f4226fde1 | |||
| 9c3f34890f | |||
| 04e2fe0bff | |||
| 83660ec016 | |||
| e38d2078cc | |||
| 895c1f132f | |||
| 1e3abf96b5 | |||
| 749c010a0d | |||
| c7e82965b1 | |||
| 5673c5f267 | |||
| 7f1252d364 | |||
| 6484dc9008 | |||
| 5c9e526f0e | |||
| f11414a424 | |||
| 6e5332f716 | |||
| a8a7f67d81 | |||
| 3b943c1582 | |||
| 2182a1cd2c | |||
| 0613062fc8 | |||
| 87474e52d7 | |||
| c55a1d9012 | |||
| a2fee6bdd3 | |||
| a3d11e8fef | |||
| 37edbd8c18 | |||
| ccdd1df039 | |||
| 72fa01f62d | |||
| d061419452 | |||
| 8f993a6200 | |||
| 7e73f57556 | |||
| d62dad5575 | |||
| 152af28a34 | |||
| 31c14ea78f | |||
| f2ea78d0e4 | |||
| 7868ef8229 | |||
| 9104ac02f7 | |||
| 394312b9c2 | |||
| 241c404573 | |||
| 9efca40dae | |||
| 584286cfd9 | |||
| d0b78225a1 | |||
| 5e01713803 | |||
| 84678c6448 | |||
| 569c977408 | |||
| b46f220931 | |||
| 6e4b84a60d | |||
| f11eb41383 | |||
| 389907ce65 | |||
| e95a6086fc | |||
| dd08582f81 | |||
| ab44824e07 | |||
| 18d6a11c04 | |||
| d6cf4d5b0b | |||
| 10b52ac4e8 | |||
| 27fdd1b6e6 | |||
| 51cd5ffd91 | |||
| f1a4e5d4d3 | |||
| 28ff62e7b1 | |||
| 6ea4479064 | |||
| 680f4cdd61 | |||
| 63757d6a7a | |||
| dcaecde844 | |||
| 0c86874677 | |||
| 76ec9122c0 | |||
| e94a10023d | |||
| 2f54aa888a | |||
| 21acdee0a0 | |||
| 89aca349cf | |||
| b4a6b16227 | |||
| 8c28250bb3 | |||
| 4e9d365407 | |||
| 9ad615c2a4 | |||
| 4262f5b0de | |||
| c80ff146d2 | |||
| 368e9ab4d8 | |||
| 2f51c22141 | |||
| 8a0a2523ca | |||
| 2285bba66e | |||
| d3a1026ae3 | |||
| 1ffe027d8a | |||
| e59e18251b | |||
| 2ebf2fbc82 | |||
| d3e43437a0 | |||
| e02868bb1a | |||
| 422c763a55 | |||
| 1d20cedf03 | |||
| 4d4cf0f2f9 | |||
| a8ac9b1fac | |||
| 78b2a28fb6 | |||
| aa386d135b | |||
| 822803d9d6 | |||
| 77afd37c2e | |||
| d99d132cdf | |||
| fdc8bd8ed9 | |||
| 13b55c6e68 | |||
| 4192cfb092 | |||
| a2f03636a5 | |||
| a14aada945 | |||
| 44aff769a3 | |||
| afc30e314f | |||
| d179b3aae4 | |||
| 4b2af10b04 | |||
| 7de790fbf2 | |||
| c313762ba0 | |||
| d85f09ac51 | |||
| 54eb1419a8 | |||
| 7108921691 | |||
| 80a445c7ae | |||
| 12059eb4ca | |||
| 9df94103ac | |||
| 00a9f654a3 | |||
| e96fd677ee | |||
| 546e033081 | |||
| fa5b616a10 | |||
| 080af01715 | |||
| c5099e9025 | |||
| e190249732 | |||
| 72f5ec3725 | |||
| d967752833 | |||
| fffa06f0b1 | |||
| e9d43b9388 | |||
| c71d6ddc3b | |||
| 5f6f6a95a2 | |||
| 8ab74ef1bb | |||
| cf84f1af68 | |||
| cf3e1a07c1 | |||
| 74801e9004 | |||
| 447826ab40 | |||
| 2ab7e3da71 | |||
| 91fa770196 | |||
| 8563e46ade | |||
| 6fb07f0b50 | |||
| 6020c760b5 | |||
| e428707e07 | |||
| 1dcf0a4cbd | |||
| 51949f3121 | |||
| 8365c8f954 | |||
| 7f20425c98 | |||
| 59b6267b2f | |||
| 58597c29d3 | |||
| 0795e55f0e | |||
| 42bedbc3d3 | |||
| 33052018a2 | |||
| c5869db080 | |||
| 467dec4edf | |||
| 4a455a053a | |||
| 34b81abd9c | |||
| 27ba28ef76 | |||
| 6d417186cb | |||
| d009267801 | |||
| 2828fc6d66 | |||
| c51d6cc9d3 | |||
| f7a2442fac | |||
| f2a8d39f42 | |||
| edc12e416d | |||
| 9dae07784b | |||
| c414512f19 | |||
| b1d693786c | |||
| b228923446 | |||
| 53bf778497 | |||
| cbc1614b84 | |||
| c350fbef7f | |||
| 1a84d8f674 | |||
| ae8e2106bb | |||
| 76cee17417 | |||
| 0dbd12e295 | |||
| 190e6be800 | |||
| bda7ec94df | |||
| 1468047081 | |||
| 7012c86336 | |||
| 98699a0ab5 | |||
| 21fba832d1 | |||
| c81148a0cc | |||
| 2e57989aec | |||
| a60e51f44d | |||
| 01768d7759 | |||
| 3291ffbe09 | |||
| 4225567303 | |||
| 1a5fd2ccb2 | |||
| 0869fd168f | |||
| cf82b6e127 | |||
| a0ba65944f | |||
| 31a7affb74 | |||
| 9f85f8ed29 | |||
| 816cc08a8f | |||
| 8c46108a85 | |||
| c5608869c0 | |||
| 9537ff4762 | |||
| 7c4a5464f6 | |||
| a3b557222a | |||
| 094b9dc474 | |||
| 8935a248f6 | |||
| b0aeeb53b1 | |||
| 2582c20e2a | |||
| 7468d3cddf | |||
| 1c066548b4 | |||
| fe049c286f | |||
| 2db5d83023 | |||
| dab7517622 | |||
| f21abdd1f0 | |||
| 4b7248fc46 | |||
| 5dbc83fabc | |||
| 4b4ba85313 | |||
| dc94a03f39 | |||
| 6a1b94529b | |||
| d74c0f581b | |||
| c413988ae0 | |||
| 9ee5eadac0 | |||
| fd6f6b02ea | |||
| 0d99398689 | |||
| 00805cb2cd | |||
| 05a49702d8 | |||
| 98dcf43214 | |||
| 27a773d5b2 | |||
| f2f2ecf9d8 | |||
| bfeaac8441 | |||
| 0d5f7686d7 | |||
| c1395a8808 | |||
| d4ae1ede39 | |||
| 16c16127e7 | |||
| 0c5673875b | |||
| 4e3c539f5e | |||
| 9595e98db8 | |||
| 3511e84dc3 | |||
| b3f52e215a | |||
| 9e1cfca53f | |||
| c92d4edb89 | |||
| 597dc86a9c | |||
| 480549e02e | |||
| 02503a3248 | |||
| 70d469ccd3 | |||
| 8440b971f5 | |||
| 7408c78dbb | |||
| 3281cbc835 | |||
| 5097b5a656 | |||
| 1a5fe16b22 | |||
| c181fc1cf3 | |||
| 48781272ee | |||
| 2e3eeaf920 | |||
| ceb25c8350 | |||
| 24da7b3ca6 | |||
| fa4a04157f | |||
| 068b505d5e | |||
| e297b56390 | |||
| b4c16d1f56 | |||
| 1c3d9d7623 | |||
| 7a3a9066f9 | |||
| 30080b9f4e | |||
| 2a2d041dcd | |||
| 6247cb8ddd | |||
| 1af19c57de | |||
| 221370acc5 | |||
| 4a6833ef49 | |||
| 197c5b2bdf | |||
| 1ff083af27 | |||
| 635666dec9 | |||
| 8590efd229 | |||
| 8f266f9652 | |||
| 91ee02898a | |||
| e993181628 | |||
| a96ff934ea | |||
| a008d81780 | |||
| f8c4276e69 | |||
| 70277591c4 | |||
| 564a213ebe | |||
| 7c3591f641 | |||
| 01971741e0 | |||
| 1d0856dcc8 | |||
| 6d3af7b97f | |||
| 2eb4b34aa7 | |||
| 3787aa78cd | |||
| 581709df80 | |||
| 0c192555bb | |||
| 7eaf850489 | |||
| fb1d13d600 | |||
| a8bfdf2d56 | |||
| 174fdce7d8 | |||
| 4e3ba687a6 | |||
| 02eec5c8ca | |||
| efee7c6cce | |||
| d74b0ac81d | |||
| 43da8bc747 | |||
| a0761f0c41 | |||
| 00ab1905e0 | |||
| 01dbc61d1c | |||
| 01c28df8b2 | |||
| e1a64b41e8 | |||
| 2ad666a484 | |||
| 8a70b98d1d | |||
| c9de822930 | |||
| e30dc716b4 | |||
| 2080af029b | |||
| cd13c5881b | |||
| bda472f147 | |||
| 7fd7c1a539 | |||
| ba7f1e1e3c | |||
| 872f308536 | |||
| 02ab0c234c | |||
| 937f473651 | |||
| 17e28036fa | |||
| 716f7875a2 | |||
| 4002e980d9 | |||
| 8b20d16ba8 | |||
| 26add7b078 | |||
| 93e89215e3 | |||
| d1210f2e0a | |||
| ee702c3139 | |||
| 6ca446bded | |||
| 9c72a3ae12 | |||
| 28acb8d495 | |||
| f2a60f729f | |||
| 323b1298f9 | |||
| a756489f86 | |||
| 96387aba83 | |||
| e1e59bf0cd | |||
| 750649eb64 | |||
| 2a3c3d00ea | |||
| 8b8fa6c1ae | |||
| 3d74a7061e | |||
| cd1dc7ec59 | |||
| db93ea736b | |||
| 6cc0087105 | |||
| e4d978da7c | |||
| 0d4e0fe647 | |||
| 3aaeb44739 | |||
| a0eb8e67c7 | |||
| f0b9199a77 | |||
| 5bddf40fd1 | |||
| aca27709df | |||
| 13a6a9a690 | |||
| 9749fcb425 | |||
| 3bf0304e31 | |||
| 76cf5e9fc1 | |||
| 4de4822219 | |||
| 324040032a | |||
| feb9dee4b1 | |||
| 5dcdbe64ab | |||
| 58e0224061 | |||
| e486d84d6a | |||
| e0b88dc8da | |||
| 0e25fdd56e | |||
| c09bad9393 | |||
| 7f7f2cd47e | |||
| 1bdec55fe1 | |||
| d9bd2b0e14 | |||
| e158a0d59f | |||
| 48c3470303 | |||
| 8af970061e | |||
| 8b959c2060 | |||
| 1728bf3f44 | |||
| c692a0c583 | |||
| 968e09f0b5 | |||
| dce7d2c4f7 | |||
| 458fd86429 | |||
| 609757f911 | |||
| 7d818b46bc | |||
| 9c46acc793 | |||
| 33743d347b | |||
| 601a61ed31 | |||
| 6cb6f47b56 | |||
| 59e8fcb409 | |||
| 58ef39e2a9 | |||
| 3674fb0c7e | |||
| 9862cf3204 | |||
| 8d9e1118c6 | |||
| 11e76cef26 | |||
| 62455ade9d | |||
| 28c3c3241d | |||
| 9efcb7741b | |||
| 6cd8f66a76 | |||
| 4cc1dde625 | |||
| f3d9a499dd | |||
| 46e955897e | |||
| c6bca64499 | |||
| 39d57ead1a | |||
| 5c6e601026 | |||
| 5b4c9e8e43 | |||
| aec6c0861e | |||
| ff36c93733 | |||
| 8ca2390fbf | |||
| a5f5d7b33a | |||
| 416813452e | |||
| 89aa1cad41 | |||
| 521708e294 | |||
| 1245fe4885 | |||
| 1fa40405ea | |||
| bfda4295c9 | |||
| 7116ab9c29 | |||
| d3303fd3a0 | |||
| c4c11f1d65 | |||
| e665d4f198 | |||
| c4fb1ad04b | |||
| 4a0f5476c0 | |||
| 3be8b6dc34 | |||
| 0ae59056d1 | |||
| 0e61a15438 | |||
| 17cce0adf4 | |||
| e2c3611c63 | |||
| 8da7a71d9a | |||
| 61ccd4f33a | |||
| 7d8392bab4 | |||
| 3217576743 | |||
| 5ec254253f | |||
| 884d6ebfd8 | |||
| df74594b8f | |||
| 8537aabba4 | |||
| 2970f0a06c | |||
| 4d5f0dc080 | |||
| cf91f72c5c | |||
| dcb67c32a5 | |||
| d159a1507e | |||
| c0eab31c02 | |||
| 6b518dc9e4 | |||
| 090198a7d6 | |||
| 28f9a2adfa | |||
| 4a6d0717a1 | |||
| e9274b2ab2 | |||
| 92c1279de6 | |||
| b8fa38a6e8 | |||
| c58f879026 | |||
| b35a3856a2 | |||
| baa26e9e2e | |||
| 41b82ce796 | |||
| 1c486a4050 | |||
| 4741c9a6eb | |||
| a685597b70 | |||
| 6b0ac084b8 | |||
| b1c449d11c | |||
| 3b025883b6 | |||
| 5db1b7622a | |||
| 06e5dfd538 | |||
| b59a940057 | |||
| 448a24746c | |||
| 6989032414 | |||
| 327bd5f836 | |||
| 13eadcea45 | |||
| d524309e3c | |||
| 34fe142894 | |||
| 43c707b4e8 | |||
| 95ab38e8d6 | |||
| 0a7f461d39 | |||
| 358281f0fd | |||
| 6b3ef9f939 | |||
| 4649026312 | |||
| f5b31fcd29 | |||
| ed0b90644a | |||
| 49b5db29b3 | |||
| 739654bb25 | |||
| 5344853344 | |||
| a588d5cd10 | |||
| 782686bcf3 | |||
| 201eb38479 | |||
| 7d13f24288 | |||
| adeda6a5b3 | |||
| e26c436d5c | |||
| c5182d5ca4 | |||
| 6812235cfa | |||
| 792cc145b1 | |||
| 77688c1358 | |||
| b8b3a288c7 | |||
| 9742f6e4a2 | |||
| 6b6eef5b80 | |||
| 2cd976987e | |||
| bb52149a06 | |||
| 415b757d4a | |||
| a0bed3e716 | |||
| 74f8f5eaa9 | |||
| 2b309a8abb | |||
| ae96b8914e | |||
| cfdbea4dc2 | |||
| 3a1abb07bf | |||
| 0556358560 | |||
| 69d1c644d9 | |||
| cc838fad44 | |||
| f237082c37 | |||
| b9da7290e1 | |||
| b809953890 | |||
| 539ab947a4 | |||
| a7679db6e9 | |||
| 53ac7952c7 | |||
| ca53565240 | |||
| a386841947 | |||
| 7e8f379dfb | |||
| a18eea8c23 | |||
| b27aae26c8 | |||
| 339d598295 | |||
| 976333f654 | |||
| 2616e965a7 | |||
| 5f5edb4c9b | |||
| 68a3020044 | |||
| 3aef883f4b | |||
| 3ec00d1689 | |||
| 9f20c5f95e | |||
| 28fc2d0de3 | |||
| cb8a7f01ae | |||
| fb23321514 | |||
| bf659f1977 | |||
| 19bdc441d6 | |||
| 889200d400 | |||
| c1b23c008a | |||
| e85db8aa3c | |||
| 28767b369f | |||
| 4793e86f04 | |||
| 4cb7386ec6 | |||
| 28518aee0a | |||
| 825cecc089 | |||
| e0de69f384 | |||
| df44ffbcff | |||
| 051ab58c50 | |||
| 0d849bf58e | |||
| 3073c67861 | |||
| cd1ddcb4f1 | |||
| 57ceadb7d8 | |||
| 30b04295d2 | |||
| 7ad6556623 | |||
| 304d1f2712 | |||
| dfa46df474 | |||
| 6703b37a93 | |||
| ebc0df6cbe | |||
| 64910527de | |||
| 491f8b28b4 | |||
| 491a9da80b | |||
| a8f679ccb5 | |||
| a9dc2772dd | |||
| a6e9bcb52d | |||
| 6d32405d74 | |||
| 74dd7fca98 | |||
| c09292efd1 | |||
| 00840f75a1 | |||
| 9de2e82b8f | |||
| 7828e813a8 | |||
| 688b2d0da7 | |||
| 5be89befef | |||
| aa65a4a1fc | |||
| d051c0fd0f | |||
| a5ba681f8d | |||
| ed12a2e133 | |||
| 4ce897d19d | |||
| 34f100d6ff | |||
| 7087c0508e | |||
| 1a64268bb0 | |||
| 86c3a3234f | |||
| 415ec91c6d | |||
| 320f54e205 | |||
| eea524f6bb | |||
| b65442a88c | |||
| 39cdba06a6 | |||
| f34ac6272c | |||
| 08463e6114 | |||
| b1b9735889 | |||
| cbba8007b2 | |||
| a94a9ce3bf | |||
| 7ea3dff49c | |||
| 8cf3e1611e | |||
| 133f39494e | |||
| 0510d06ecf | |||
| 4dc98b3c7e | |||
| deb11efa89 | |||
| d394a9f39f | |||
| d97d2a4f7b | |||
| 19ed2ed630 | |||
| 5790a5d7cf | |||
| d8c0587346 | |||
| aed42a726a | |||
| 30bdef9bf5 | |||
| fb1c67219d | |||
| bf6e809abf | |||
| 31045e6086 | |||
| c24dc0ec77 | |||
| cf89c030d0 | |||
| 105c20146c | |||
| 09fabe3021 | |||
| cf92ffab34 | |||
| d771dcbdb9 | |||
| 095163bbed | |||
| 94c04955c8 | |||
| 72d0b73598 | |||
| 4c951ea435 | |||
| 7f3798e180 | |||
| 60bee4b137 | |||
| acc655d35f | |||
| f90dcf663e | |||
| 4c2a5045a0 | |||
| e163e02499 | |||
| ffe368afed | |||
| 9bc9c6e6c5 | |||
| ef7dcdb49e | |||
| 05c568126f | |||
| 8cfd915960 | |||
| 93458727e8 | |||
| a792c4a159 | |||
| 5e34d9e276 | |||
| 60fc979332 | |||
| 8e0fb9ee2f | |||
| 3db2a796ec | |||
| 5c5c470671 | |||
| 80b04dc505 | |||
| 1ed9743ad4 | |||
| a7fba66832 | |||
| 79710375e3 | |||
| 98fa8d2b51 | |||
| 1eeadcd85c | |||
| f9c4014e28 | |||
| 1a167b2ea5 | |||
| cacf0cc0ef | |||
| dd81be1b9b | |||
| 843d7c1fe3 | |||
| 1340c7a792 | |||
| 319f211211 | |||
| 7663ccf0bd | |||
| 3f048bce0f | |||
| dec9726083 | |||
| 09973956ae | |||
| ddaa21c750 | |||
| d4614619b4 | |||
| baf39042c8 | |||
| 2571e07175 | |||
| 53a5728009 | |||
| dea0782c89 | |||
| 651a543403 | |||
| 738c2692fb | |||
| 618f8a43cf | |||
| 0a531f732b | |||
| 7f95c594c0 | |||
| fad526c63f | |||
| 7588aef07c | |||
| 81f0f618f7 | |||
| e351baf10f | |||
| 39bdedab9c | |||
| bfb064024e | |||
| ce0f2dd868 | |||
| d668600672 | |||
| 63f864cdd7 | |||
| c095091853 | |||
| d55168f51f | |||
| 305ed41b88 | |||
| ef909f6335 | |||
| c80800a3ee | |||
| 9d608135e3 | |||
| 8180ed9a68 | |||
| d6556c5246 | |||
| a76d9b4dcf | |||
| f480ef4bbc | |||
| a66ad2e2af | |||
| 99f5ed9ecb | |||
| 044ccc6dd7 | |||
| f7edf71190 | |||
| 3fce6cea27 | |||
| 0c9e1ef61b | |||
| d9ad2a74ae | |||
| ce1c83da89 | |||
| 75ecb4a81f | |||
| 2f5f6baf0f | |||
| d2ef83bc60 | |||
| 24371a3954 | |||
| 65e8e3ed1f | |||
| 4354458cad | |||
| e75f0722e7 | |||
| 3adc0dfbaf | |||
| f3669f20a9 | |||
| e5a50d0154 | |||
| 6e4d4fc604 | |||
| 40e11e053c | |||
| 2a850ed051 | |||
| 383b917784 | |||
| fa264e4286 | |||
| 23b43ff651 | |||
| e7fa39112a | |||
| dfae3f6284 | |||
| 52683dafc3 | |||
| f51ca774cf | |||
| 482aeaff10 | |||
| 3ed61f1ff2 | |||
| 56d4a35d05 | |||
| abb60a4d10 | |||
| 328846c6e3 | |||
| 3afaa8033b | |||
| e1fac40256 | |||
| 7cd3b95317 | |||
| 93fd6a9160 | |||
| f6a4a5c44d | |||
| bb95c8c45a | |||
| dbf4c3a37c | |||
| f330a87e50 | |||
| f72aa8c840 | |||
| 999d0568fa | |||
| 7878f54043 | |||
| 97db77997f | |||
| 3829ac6353 | |||
| 1bfc62dcc2 | |||
| e0339993ae | |||
| cb30351403 | |||
| b56d9c8639 | |||
| 00d29aa162 | |||
| cfc697a96d | |||
| 7928c1727f | |||
| d3ee91ff92 | |||
| a280727248 | |||
| d970882428 | |||
| ece8630c33 | |||
| 81ba61df7f | |||
| 898a83031c |
@@ -0,0 +1,89 @@
|
||||
# Use a common base image like Debian.
|
||||
# Using 'bookworm-slim' for a balance of size and compatibility.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Set environment variables to prevent interactive prompts during installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV NODE_VERSION=20.12.2
|
||||
ENV NODE_VERSION_MAJOR=20
|
||||
ENV DOCKER_CLI_VERSION=26.1.3
|
||||
ENV BUILDX_VERSION=v0.14.0
|
||||
|
||||
# Install dependencies for adding NodeSource repository, gcloud, and other tools
|
||||
# - curl: for downloading files
|
||||
# - gnupg: for managing GPG keys (used by NodeSource & Google Cloud SDK)
|
||||
# - apt-transport-https: for HTTPS apt repositories
|
||||
# - ca-certificates: for HTTPS apt repositories
|
||||
# - rsync: the rsync utility itself
|
||||
# - git: often useful in build environments
|
||||
# - python3, python3-pip, python3-venv, python3-crcmod: for gcloud SDK and some of its components
|
||||
# - lsb-release: for gcloud install script to identify distribution
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
gnupg \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
rsync \
|
||||
git \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-crcmod \
|
||||
lsb-release \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node.js and npm
|
||||
# We'll use the official NodeSource repository for a specific version
|
||||
RUN set -eux; \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
# For Node.js 20.x, it's node_20.x
|
||||
# Let's explicitly define the major version for clarity
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
npm install -g npm@latest && \
|
||||
# Verify installations
|
||||
node -v && \
|
||||
npm -v && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Docker CLI
|
||||
# Download the static binary from Docker's official source
|
||||
RUN set -eux; \
|
||||
DOCKER_CLI_ARCH=$(dpkg --print-architecture); \
|
||||
case "${DOCKER_CLI_ARCH}" in \
|
||||
amd64) DOCKER_CLI_ARCH_SUFFIX="x86_64" ;; \
|
||||
arm64) DOCKER_CLI_ARCH_SUFFIX="aarch64" ;; \
|
||||
*) echo "Unsupported architecture: ${DOCKER_CLI_ARCH}"; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL "https://download.docker.com/linux/static/stable/${DOCKER_CLI_ARCH_SUFFIX}/docker-${DOCKER_CLI_VERSION}.tgz" -o docker.tgz && \
|
||||
tar -xzf docker.tgz --strip-components=1 -C /usr/local/bin docker/docker && \
|
||||
rm docker.tgz && \
|
||||
# Verify installation
|
||||
docker --version
|
||||
|
||||
# Install Docker Buildx plugin
|
||||
RUN set -eux; \
|
||||
BUILDX_ARCH_DEB=$(dpkg --print-architecture); \
|
||||
case "${BUILDX_ARCH_DEB}" in \
|
||||
amd64) BUILDX_ARCH_SUFFIX="amd64" ;; \
|
||||
arm64) BUILDX_ARCH_SUFFIX="arm64" ;; \
|
||||
*) echo "Unsupported architecture for Buildx: ${BUILDX_ARCH_DEB}"; exit 1 ;; \
|
||||
esac; \
|
||||
mkdir -p /usr/local/lib/docker/cli-plugins && \
|
||||
curl -fsSL "https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-${BUILDX_ARCH_SUFFIX}" -o /usr/local/lib/docker/cli-plugins/docker-buildx && \
|
||||
chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx && \
|
||||
# verify installation
|
||||
docker buildx version
|
||||
|
||||
# Install Google Cloud SDK (gcloud CLI)
|
||||
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg && apt-get update -y && apt-get install google-cloud-cli -y
|
||||
|
||||
# Set a working directory (optional, but good practice)
|
||||
WORKDIR /workspace
|
||||
|
||||
# You can add a CMD or ENTRYPOINT if you intend to run this image directly,
|
||||
# but for Cloud Build, it's usually not necessary as Cloud Build steps override it.
|
||||
# For example:
|
||||
ENTRYPOINT '/bin/bash'
|
||||
@@ -0,0 +1,47 @@
|
||||
steps:
|
||||
# Step 1: Install root dependencies (includes workspaces)
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['install']
|
||||
|
||||
# Step 2: Update version in root package.json
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- -c # Use bash -c to allow for command substitution and string manipulation
|
||||
- |
|
||||
current_version=$(npm pkg get version | sed 's/"//g')
|
||||
new_version="$${current_version}-$SHORT_SHA.$_REVISION"
|
||||
npm pkg set "version=$${new_version}"
|
||||
echo "Set root package.json version to: $${new_version}"
|
||||
|
||||
# Step 3: Run prerelease:dev to update workspace versions and dependencies
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'prerelease:dev'] # This will run prerelease:version and prerelease:deps
|
||||
|
||||
# Step 4: Authenticate for Docker and NPM
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'auth']
|
||||
|
||||
# Step 5: Run the master release script
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'publish:release']
|
||||
env:
|
||||
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
|
||||
- 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
|
||||
- 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
|
||||
- 'NPM_PUBLISH_TAG=$_NPM_PUBLISH_TAG'
|
||||
|
||||
options:
|
||||
defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET
|
||||
dynamicSubstitutions: true
|
||||
|
||||
substitutions:
|
||||
_REVISION: '0'
|
||||
_SANDBOX_IMAGE_REGISTRY: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers'
|
||||
_SANDBOX_IMAGE_NAME: 'gemini-cli-sandbox'
|
||||
_NPM_PUBLISH_TAG: 'head'
|
||||
_CONTAINER_TOOL: 'docker'
|
||||
@@ -0,0 +1,30 @@
|
||||
steps:
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['install']
|
||||
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'auth']
|
||||
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args:
|
||||
[
|
||||
'run',
|
||||
'prerelease:version',
|
||||
'--workspaces',
|
||||
'--',
|
||||
'--suffix="$SHORT_SHA.$_REVISION"',
|
||||
]
|
||||
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'prerelease:deps', '--workspaces']
|
||||
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['publish', '--tag=head', '--dry-run', '--workspace=@gemini-cli/cli']
|
||||
|
||||
options:
|
||||
defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET
|
||||
@@ -0,0 +1,10 @@
|
||||
have_fun: false
|
||||
code_review:
|
||||
disable: true
|
||||
comment_severity_threshold: HIGH
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: false
|
||||
summary: false
|
||||
code_review: false
|
||||
ignore_patterns: []
|
||||
@@ -0,0 +1,7 @@
|
||||
# By default, require reviews from the release approvers for all files.
|
||||
* @google-gemini/gemini-cli-askmode-approvers
|
||||
|
||||
# The following files don't need reviews from the release approvers.
|
||||
# These patterns override the rule above.
|
||||
**/*.md
|
||||
/docs/
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,102 @@
|
||||
name: 'Post Coverage Comment Action'
|
||||
description: 'Prepares and posts a code coverage comment to a PR.'
|
||||
|
||||
inputs:
|
||||
cli_json_file:
|
||||
description: 'Path to CLI coverage-summary.json'
|
||||
required: true
|
||||
core_json_file:
|
||||
description: 'Path to Core coverage-summary.json'
|
||||
required: true
|
||||
cli_full_text_summary_file:
|
||||
description: 'Path to CLI full-text-summary.txt'
|
||||
required: true
|
||||
core_full_text_summary_file:
|
||||
description: 'Path to Core full-text-summary.txt'
|
||||
required: true
|
||||
node_version:
|
||||
description: 'Node.js version for context in messages'
|
||||
required: true
|
||||
github_token:
|
||||
description: 'GitHub token for posting comments'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Prepare Coverage Comment
|
||||
id: prep_coverage_comment
|
||||
shell: bash
|
||||
run: |
|
||||
cli_json_file="${{ inputs.cli_json_file }}"
|
||||
core_json_file="${{ inputs.core_json_file }}"
|
||||
cli_full_text_summary_file="${{ inputs.cli_full_text_summary_file }}"
|
||||
core_full_text_summary_file="${{ inputs.core_full_text_summary_file }}"
|
||||
comment_file="coverage-comment.md"
|
||||
|
||||
# Extract percentages using jq for the main table
|
||||
if [ -f "$cli_json_file" ]; then
|
||||
cli_lines_pct=$(jq -r '.total.lines.pct' "$cli_json_file")
|
||||
cli_statements_pct=$(jq -r '.total.statements.pct' "$cli_json_file")
|
||||
cli_functions_pct=$(jq -r '.total.functions.pct' "$cli_json_file")
|
||||
cli_branches_pct=$(jq -r '.total.branches.pct' "$cli_json_file")
|
||||
else
|
||||
cli_lines_pct="N/A"; cli_statements_pct="N/A"; cli_functions_pct="N/A"; cli_branches_pct="N/A"
|
||||
echo "CLI coverage-summary.json not found at: $cli_json_file" >&2 # Error to stderr
|
||||
fi
|
||||
|
||||
if [ -f "$core_json_file" ]; then
|
||||
core_lines_pct=$(jq -r '.total.lines.pct' "$core_json_file")
|
||||
core_statements_pct=$(jq -r '.total.statements.pct' "$core_json_file")
|
||||
core_functions_pct=$(jq -r '.total.functions.pct' "$core_json_file")
|
||||
core_branches_pct=$(jq -r '.total.branches.pct' "$core_json_file")
|
||||
else
|
||||
core_lines_pct="N/A"; core_statements_pct="N/A"; core_functions_pct="N/A"; core_branches_pct="N/A"
|
||||
echo "Core coverage-summary.json not found at: $core_json_file" >&2 # Error to stderr
|
||||
fi
|
||||
|
||||
echo "## Code Coverage Summary" > "$comment_file"
|
||||
echo "" >> "$comment_file"
|
||||
echo "| Package | Lines | Statements | Functions | Branches |" >> "$comment_file"
|
||||
echo "|---|---|---|---|---|" >> "$comment_file"
|
||||
echo "| CLI | ${cli_lines_pct}% | ${cli_statements_pct}% | ${cli_functions_pct}% | ${cli_branches_pct}% |" >> "$comment_file"
|
||||
echo "| Core | ${core_lines_pct}% | ${core_statements_pct}% | ${core_functions_pct}% | ${core_branches_pct}% |" >> "$comment_file"
|
||||
echo "" >> "$comment_file"
|
||||
|
||||
# CLI Package - Collapsible Section (with full text summary from file)
|
||||
echo "<details>" >> "$comment_file"
|
||||
echo "<summary>CLI Package - Full Text Report</summary>" >> "$comment_file"
|
||||
echo "" >> "$comment_file"
|
||||
echo '```text' >> "$comment_file"
|
||||
if [ -f "$cli_full_text_summary_file" ]; then
|
||||
cat "$cli_full_text_summary_file" >> "$comment_file"
|
||||
else
|
||||
echo "CLI full-text-summary.txt not found at: $cli_full_text_summary_file" >> "$comment_file"
|
||||
fi
|
||||
echo '```' >> "$comment_file"
|
||||
echo "</details>" >> "$comment_file"
|
||||
echo "" >> "$comment_file"
|
||||
|
||||
# Core Package - Collapsible Section (with full text summary from file)
|
||||
echo "<details>" >> "$comment_file"
|
||||
echo "<summary>Core Package - Full Text Report</summary>" >> "$comment_file"
|
||||
echo "" >> "$comment_file"
|
||||
echo '```text' >> "$comment_file"
|
||||
if [ -f "$core_full_text_summary_file" ]; then
|
||||
cat "$core_full_text_summary_file" >> "$comment_file"
|
||||
else
|
||||
echo "Core full-text-summary.txt not found at: $core_full_text_summary_file" >> "$comment_file"
|
||||
fi
|
||||
echo '```' >> "$comment_file"
|
||||
echo "</details>" >> "$comment_file"
|
||||
echo "" >> "$comment_file"
|
||||
|
||||
echo "_For detailed HTML reports, please see the 'coverage-reports-${{ inputs.node_version }}' artifact from the main CI run._" >> "$comment_file"
|
||||
|
||||
- name: Post Coverage Comment
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
if: always()
|
||||
with:
|
||||
file-path: coverage-comment.md # Use the generated file directly
|
||||
comment-tag: code-coverage-summary
|
||||
github-token: ${{ inputs.github_token }}
|
||||
@@ -0,0 +1,27 @@
|
||||
## TLDR
|
||||
|
||||
<!-- Add a brief description of what this pull request changes and why and any important things for reviewers to look at -->
|
||||
|
||||
## Dive Deeper
|
||||
|
||||
<!-- more thoughts and in depth discussion here -->
|
||||
|
||||
## Reviewer Test Plan
|
||||
|
||||
<!-- when a person reviewes your code they should ideally be pulling and running that code. How would they validate your change works and if relevant what are some good classes of example prompts and ways they can exercise your changes -->
|
||||
|
||||
## Testing Matrix
|
||||
|
||||
<!-- Before submitting please validate your changes on as many of these options as possible -->
|
||||
|
||||
| | 🍏 | 🪟 | 🐧 |
|
||||
| -------- | --- | --- | --- |
|
||||
| npm run | ❓ | ❓ | ❓ |
|
||||
| npx | ❓ | ❓ | ❓ |
|
||||
| Docker | ❓ | ❓ | ❓ |
|
||||
| Podman | ❓ | - | - |
|
||||
| Seatbelt | ❓ | - | - |
|
||||
|
||||
## Linked issues / bugs
|
||||
|
||||
<!-- Add links to any gh issues or other external bugs --->
|
||||
@@ -0,0 +1,145 @@
|
||||
# .github/workflows/ci.yml
|
||||
|
||||
name: Gemini CLI CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release]
|
||||
pull_request:
|
||||
branches: [main, release]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Lint
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # For checkout
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run formatter check
|
||||
run: |
|
||||
npm run format
|
||||
git diff --exit-code
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Run type check
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts-${{ matrix.node-version }}
|
||||
path: |
|
||||
packages/*/dist
|
||||
package-lock.json # Only upload dist and lockfile
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: build # This job depends on the 'build' job
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x] # Should match the build job's matrix
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts-${{ matrix.node-version }}
|
||||
path: . # Download to the root, this will include package-lock.json and packages/*/dist
|
||||
|
||||
# Restore/create package structure for dist folders if necessary.
|
||||
# The download-artifact action with path: . should place them correctly if the
|
||||
# upload paths were relative to the workspace root.
|
||||
# Example: if uploaded `packages/cli/dist`, it will be at `./packages/cli/dist`.
|
||||
|
||||
- name: Install dependencies for testing
|
||||
run: npm ci # Install fresh dependencies using the downloaded package-lock.json
|
||||
|
||||
- name: Run tests and generate reports
|
||||
run: NO_COLOR=true npm run test:ci
|
||||
|
||||
- name: Publish Test Report (for non-forks)
|
||||
if: always() && (github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: dorny/test-reporter@v1
|
||||
with:
|
||||
name: Test Results (Node ${{ matrix.node-version }})
|
||||
path: packages/*/junit.xml
|
||||
reporter: java-junit
|
||||
fail-on-error: 'false'
|
||||
|
||||
- name: Upload Test Results Artifact (for forks)
|
||||
if: always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-fork-${{ matrix.node-version }}
|
||||
path: packages/*/junit.xml
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: coverage-reports-${{ matrix.node-version }}
|
||||
path: packages/*/coverage
|
||||
|
||||
post_coverage_comment:
|
||||
name: Post Coverage Comment
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: always() && github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name == github.repository)
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read # For checkout
|
||||
pull-requests: write # For commenting
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x] # Should match the test job's matrix
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download coverage reports artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: coverage-reports-${{ matrix.node-version }}
|
||||
path: coverage_artifact # Download to a specific directory
|
||||
|
||||
- name: Post Coverage Comment using Composite Action
|
||||
uses: ./.github/actions/post-coverage-comment # Path to the composite action directory
|
||||
with:
|
||||
cli_json_file: coverage_artifact/cli/coverage/coverage-summary.json
|
||||
core_json_file: coverage_artifact/core/coverage/coverage-summary.json
|
||||
cli_full_text_summary_file: coverage_artifact/cli/coverage/full-text-summary.txt
|
||||
core_full_text_summary_file: coverage_artifact/core/coverage/full-text-summary.txt
|
||||
node_version: ${{ matrix.node-version }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,49 @@
|
||||
# .github/workflows/e2e.yml
|
||||
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release]
|
||||
pull_request:
|
||||
branches: [main, release]
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
name: E2E Test - ${{ matrix.sandbox }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
sandbox: [sandbox:none, sandbox:docker]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Set up Docker
|
||||
if: matrix.sandbox == 'sandbox:docker'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Podman
|
||||
if: matrix.sandbox == 'sandbox:podman'
|
||||
uses: redhat-actions/podman-login@v1
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Run E2E tests
|
||||
env:
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
run: npm run test:integration:${{ matrix.sandbox }} -- --verbose --keep-output
|
||||
@@ -1,5 +1,10 @@
|
||||
# API keys and secrets
|
||||
.env
|
||||
.env~
|
||||
|
||||
# gemini-cli settings
|
||||
.gemini/
|
||||
!gemini/config.yaml
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
@@ -13,5 +18,21 @@ bower_components
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# TypeScript build info files
|
||||
*.tsbuildinfo
|
||||
|
||||
# Ignore built ts files
|
||||
dist
|
||||
dist
|
||||
|
||||
# Docker folder to help skip auth refreshes
|
||||
.docker
|
||||
|
||||
bundle
|
||||
|
||||
# Test report files
|
||||
junit.xml
|
||||
packages/*/coverage/
|
||||
|
||||
# Generated files
|
||||
packages/cli/src/generated/
|
||||
.integration-tests/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@gemini-cli:registry=https://us-west1-npm.pkg.dev/gemini-code-dev/gemini-code/
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -1,29 +1,77 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach",
|
||||
"port": 9229,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${file}",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch CLI",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "start"],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"GEMINI_SANDBOX": "false",
|
||||
"GEMINI_API_KEY": "testkey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch E2E",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "test:e2e", "read_many_files"],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "Attach",
|
||||
"port": 9229,
|
||||
"request": "attach",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "node",
|
||||
// fix source mapping when debugging in sandbox using global installation
|
||||
// note this does not interfere when remoteRoot is also ${workspaceFolder}/packages
|
||||
"remoteRoot": "/usr/local/share/npm-global/lib/node_modules/@gemini-cli",
|
||||
"localRoot": "${workspaceFolder}/packages"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${file}",
|
||||
"outFiles": ["${workspaceFolder}/**/*.js"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Test File",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"test",
|
||||
"-w",
|
||||
"packages",
|
||||
"--",
|
||||
"--inspect-brk=9229",
|
||||
"--no-file-parallelism",
|
||||
"${input:testFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "testFile",
|
||||
"type": "promptString",
|
||||
"description": "Enter the path to the test file (e.g., ${workspaceFolder}/packages/cli/src/ui/components/LoadingIndicator.test.tsx)",
|
||||
"default": "${workspaceFolder}/packages/cli/src/ui/components/LoadingIndicator.test.tsx"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"label": "npm: build",
|
||||
"detail": "scripts/build.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
# How to Contribute
|
||||
|
||||
We would love to accept your patches and contributions to this project.
|
||||
|
||||
## Before you begin
|
||||
|
||||
### Sign our Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a
|
||||
[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
|
||||
You (or your employer) retain the copyright to your contribution; this simply
|
||||
gives us permission to use and redistribute your contributions as part of the
|
||||
project.
|
||||
|
||||
If you or your current employer have already signed the Google CLA (even if it
|
||||
was for a different project), you probably don't need to do it again.
|
||||
|
||||
Visit <https://cla.developers.google.com/> to see your current agreements or to
|
||||
sign a new one.
|
||||
|
||||
### Review our Community Guidelines
|
||||
|
||||
This project follows [Google's Open Source Community
|
||||
Guidelines](https://opensource.google/conduct/).
|
||||
|
||||
## Contribution Process
|
||||
|
||||
### Code Reviews
|
||||
|
||||
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.
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
To help us review and merge your PRs quickly, please follow these guidelines. PRs that do not meet these standards may be closed.
|
||||
|
||||
#### 1. Link to an Existing Issue
|
||||
|
||||
All PRs should be linked to an existing issue in our tracker. This ensures that every change has been discussed and is aligned with the project's goals before any code is written.
|
||||
|
||||
- **For bug fixes:** The PR should be linked to the bug report issue.
|
||||
- **For features:** The PR should be linked to the feature request or proposal issue that has been approved by a maintainer.
|
||||
|
||||
If an issue for your change doesn't exist, please **open one first** and wait for feedback before you start coding.
|
||||
|
||||
#### 2. Keep It Small and Focused
|
||||
|
||||
We favor small, atomic PRs that address a single issue or add a single, self-contained feature.
|
||||
|
||||
- **Do:** Create a PR that fixes one specific bug or adds one specific feature.
|
||||
- **Don't:** Bundle multiple unrelated changes (e.g., a bug fix, a new feature, and a refactor) into a single PR.
|
||||
|
||||
Large changes should be broken down into a series of smaller, logical PRs that can be reviewed and merged independently.
|
||||
|
||||
#### 3. Use Draft PRs for Work in Progress
|
||||
|
||||
If you'd like to get early feedback on your work, please use GitHub's **Draft Pull Request** feature. This signals to the maintainers that the PR is not yet ready for a formal review but is open for discussion and initial feedback.
|
||||
|
||||
#### 4. Ensure All Checks Pass
|
||||
|
||||
Before submitting your PR, ensure that all automated checks are passing by running `npm run preflight`. This command runs all tests, linting, and other style checks.
|
||||
|
||||
#### 5. Update Documentation
|
||||
|
||||
If your PR introduces a user-facing change (e.g., a new command, a modified flag, or a change in behavior), you must also update the relevant documentation in the `/docs` directory.
|
||||
|
||||
#### 6. Write Clear Commit Messages and a Good PR Description
|
||||
|
||||
Your PR should have a clear, descriptive title and a detailed description of the changes. Follow the [Conventional Commits](https://www.conventionalcommits.org/) standard for your commit messages.
|
||||
|
||||
- **Good PR Title:** `feat(cli): Add --json flag to 'config get' command`
|
||||
- **Bad PR Title:** `Made some changes`
|
||||
|
||||
In the PR description, explain the "why" behind your changes and link to the relevant issue (e.g., `Fixes #123`).
|
||||
|
||||
## Forking
|
||||
|
||||
If you are forking the repository you will be able to run the Built, Test and Integration test workflows. However in order to make the integration tests run you'll need to add a [Github Repository Secret](<[url](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)>) with a value of `GEMINI_API_KEY` and set that to a valid API key that you have available. Your key and secret it private to your repo; no one without access can see your key and you cannot see any secrets related to this repo.
|
||||
|
||||
Additionally you will need to click on the `Actions` tab and enable workflows for your repository, you'll find its the large blue button in the center of the screen.
|
||||
|
||||
## Development Setup and Workflow
|
||||
|
||||
This section guides contributors on how to build, modify, and understand the development setup of this project.
|
||||
|
||||
### Setting Up the Development Environment
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
1. Install [Node 18+](https://nodejs.org/en/download)
|
||||
2. Git
|
||||
|
||||
### Build Process
|
||||
|
||||
To clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/google-gemini/gemini-cli.git # Or your fork's URL
|
||||
cd gemini-cli
|
||||
```
|
||||
|
||||
To install dependencies defined in `package.json` as well as root dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
To build the entire project (all packages):
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This command typically compiles TypeScript to JavaScript, bundles assets, and prepares the packages for execution. Refer to `scripts/build.js` and `package.json` scripts for more details on what happens during the build.
|
||||
|
||||
### Enabling Sandboxing
|
||||
|
||||
Container-based [sandboxing](#sandboxing) is highly recommended and requires, at a minimum, setting `GEMINI_SANDBOX=true` in your `~/.env` and ensuring a container engine (e.g. `docker` or `podman`) is available. See [Sandboxing](#sandboxing) for details.
|
||||
|
||||
To build both the `gemini` CLI utility and the sandbox container, run `build:all` from the root directory:
|
||||
|
||||
```bash
|
||||
npm run build:all
|
||||
```
|
||||
|
||||
To skip building the sandbox container, you can use `npm run build` instead.
|
||||
|
||||
### Running
|
||||
|
||||
To start the Gemini CLI from the source code (after building), run the following command from the root directory:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
If you’d like to run the source build outside of the gemini-cli folder you can utilize `npm link path/to/gemini-cli/packages/cli` (see: [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) or `alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`
|
||||
|
||||
### Running Tests
|
||||
|
||||
This project contains two types of tests: unit tests and integration tests.
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
To execute the unit test suite for the project:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
This will run tests located in the `packages/core` and `packages/cli` directories. Ensure tests pass before submitting any changes. For a more comprehensive check, it is recommended to run `npm run preflight`.
|
||||
|
||||
#### Integration Tests
|
||||
|
||||
The integration tests are designed to validate the end-to-end functionality of the Gemini CLI. They are not run as part of the default `npm run test` command.
|
||||
|
||||
To run the integration tests, use the following command:
|
||||
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
For more detailed information on the integration testing framework, please see the [Integration Tests documentation](./docs/integration-tests.md).
|
||||
|
||||
### Linting and Preflight Checks
|
||||
|
||||
To ensure code quality and formatting consistency, run the preflight check:
|
||||
|
||||
```bash
|
||||
npm run preflight
|
||||
```
|
||||
|
||||
This command will run ESLint, Prettier, all tests, and other checks as defined in the project's `package.json`.
|
||||
|
||||
_ProTip_
|
||||
|
||||
after cloning create a git precommit hook file to ensure your commits are always clean.
|
||||
|
||||
```bash
|
||||
echo "
|
||||
# Run npm build and check for errors
|
||||
if ! npm run preflight; then
|
||||
echo "npm build failed. Commit aborted."
|
||||
exit 1
|
||||
fi
|
||||
" > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
#### Formatting
|
||||
|
||||
To separately format the code in this project by running the following command from the root directory:
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
```
|
||||
|
||||
This command uses Prettier to format the code according to the project's style guidelines.
|
||||
|
||||
#### Linting
|
||||
|
||||
To separately lint the code in this project, run the following command fro the root directory:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Coding Conventions
|
||||
|
||||
- Please adhere to the coding style, patterns, and conventions used throughout the existing codebase.
|
||||
- Consult [GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md) (typically found in the project root) for specific instructions related to AI-assisted development, including conventions for React, comments, and Git usage.
|
||||
- **Imports:** Pay special attention to import paths. The project uses `eslint-rules/no-relative-cross-package-imports.js` to enforce restrictions on relative imports between packages.
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `packages/`: Contains the individual sub-packages of the project.
|
||||
- `cli/`: The command-line interface.
|
||||
- `server/`: The backend server that the CLI interacts with.
|
||||
- `docs/`: Contains all project documentation.
|
||||
- `scripts/`: Utility scripts for building, testing, and development tasks.
|
||||
|
||||
For more detailed architecture, see `docs/architecture.md`.
|
||||
|
||||
## Debugging
|
||||
|
||||
### VS Code:
|
||||
|
||||
0. Run the CLI to interactively debug in VS Code with `F5`
|
||||
1. Start the CLI in debug mode from the root directory:
|
||||
```bash
|
||||
npm run debug
|
||||
```
|
||||
This command runs `node --inspect-brk dist/gemini.js` within the `packages/cli` directory, pausing execution until a debugger attaches. You can then open `chrome://inspect` in your Chrome browser to connect to the debugger.
|
||||
2. In VS Code, use the "Attach" launch configuration (found in `.vscode/launch.json`).
|
||||
|
||||
Alternatively, you can use the "Launch Program" configuration in VS Code if you prefer to launch the currently open file directly, but 'F5' is generally recommended.
|
||||
|
||||
To hit a breakpoint inside the sandbox container run:
|
||||
|
||||
```bash
|
||||
DEBUG=1 gemini
|
||||
```
|
||||
|
||||
### React DevTools
|
||||
|
||||
To debug the CLI's React-based UI, you can use React DevTools. Ink, the library used for the CLI's interface, is compatible with React DevTools version 4.x.
|
||||
|
||||
1. **Start the Gemini CLI in development mode:**
|
||||
|
||||
```bash
|
||||
DEV=true npm start
|
||||
```
|
||||
|
||||
2. **Install and run React DevTools version 4.28.5 (or the latest compatible 4.x version):**
|
||||
|
||||
You can either install it globally:
|
||||
|
||||
```bash
|
||||
npm install -g react-devtools@4.28.5
|
||||
react-devtools
|
||||
```
|
||||
|
||||
Or run it directly using npx:
|
||||
|
||||
```bash
|
||||
npx react-devtools@4.28.5
|
||||
```
|
||||
|
||||
Your running CLI application should then connect to React DevTools.
|
||||

|
||||
|
||||
## Sandboxing
|
||||
|
||||
### MacOS Seatbelt
|
||||
|
||||
On MacOS, `gemini` uses Seatbelt (`sandbox-exec`) under a `permissive-open` profile (see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) that restricts writes to the project folder but otherwise allows all other operations and outbound network traffic ("open") by default. You can switch to a `restrictive-closed` profile (see `.../sandbox-macos-strict.sb`) that declines all operations and outbound network traffic ("closed") by default by setting `SEATBELT_PROFILE=restrictive-closed` in your environment or `.env` file. Available built-in profiles are `{permissive,restrictive}-{open,closed,proxied}` (see below for proxied networking). You can also switch to a custom profile `SEATBELT_PROFILE=<profile>` if you also create a file `.gemini/sandbox-macos-<profile>.sb` under your project settings directory `.gemini`.
|
||||
|
||||
### Container-based Sandboxing (All Platforms)
|
||||
|
||||
For stronger container-based sandboxing on MacOS or other platforms, you can set `GEMINI_SANDBOX=true|docker|podman|<command>` in your environment or `.env` file. The specified command (or if `true` then either `docker` or `podman`) must be installed on the host machine. Once enabled, `npm run build:all` will build a minimal container ("sandbox") image and `npm start` will launch inside a fresh instance of that container. The first build can take 20-30s (mostly due to downloading of the base image) but after that both build and start overhead should be minimal. Default builds (`npm run build`) will not rebuild the sandbox.
|
||||
|
||||
Container-based sandboxing mounts the project directory (and system temp directory) with read-write access and is started/stopped/removed automatically as you start/stop Gemini CLI. Files created within the sandbox should be automatically mapped to your user/group on host machine. You can easily specify additional mounts, ports, or environment variables by setting `SANDBOX_{MOUNTS,PORTS,ENV}` as needed. You can also fully customize the sandbox for your projects by creating the files `.gemini/sandbox.Dockerfile` and/or `.gemini/sandbox.bashrc` under your project settings directory (`.gemini`) and running `gemini` with `BUILD_SANDBOX=1` to trigger building of your custom sandbox.
|
||||
|
||||
#### Proxied Networking
|
||||
|
||||
All sandboxing methods, including MacOS Seatbelt using `*-proxied` profiles, support restricting outbound network traffic through a custom proxy server that can be specified as `GEMINI_SANDBOX_PROXY_COMMAND=<command>`, where `<command>` must start a proxy server that listens on `:::8877` for relevant requests. See `scripts/example-proxy.js` for a minimal proxy that only allows `HTTPS` connections to `example.com:443` (e.g. `curl https://example.com`) and declines all other requests. The proxy is started and stopped automatically alongside the sandbox.
|
||||
|
||||
## Manual Publish
|
||||
|
||||
We publish an artifact for each commit to our internal registry. But if you need to manually cut a local build, then run the following commands:
|
||||
|
||||
```
|
||||
npm run clean
|
||||
npm install
|
||||
npm run auth
|
||||
npm run prerelease:dev
|
||||
npm publish --workspaces
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
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 minimal set of packages, then clean up
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
man-db \
|
||||
curl \
|
||||
dnsutils \
|
||||
less \
|
||||
jq \
|
||||
bc \
|
||||
gh \
|
||||
git \
|
||||
unzip \
|
||||
rsync \
|
||||
ripgrep \
|
||||
procps \
|
||||
psmisc \
|
||||
lsof \
|
||||
socat \
|
||||
ca-certificates \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 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
|
||||
|
||||
# install gemini-cli and clean up
|
||||
COPY packages/cli/dist/gemini-cli-cli-*.tgz /usr/local/share/npm-global/gemini-cli-cli.tgz
|
||||
COPY packages/core/dist/gemini-cli-core-*.tgz /usr/local/share/npm-global/gemini-cli-core.tgz
|
||||
RUN npm install -g /usr/local/share/npm-global/gemini-cli-cli.tgz /usr/local/share/npm-global/gemini-cli-core.tgz \
|
||||
&& npm cache clean --force \
|
||||
&& rm -f /usr/local/share/npm-global/gemini-cli-{cli,core}.tgz
|
||||
|
||||
# default entrypoint when none specified
|
||||
CMD ["gemini"]
|
||||
@@ -0,0 +1,182 @@
|
||||
## Building and running
|
||||
|
||||
Before submitting any changes, it is crucial to validate them by running the full preflight check. This command will build the repository, run all tests, check for type errors, and lint the code.
|
||||
|
||||
To run the full suite of checks, execute the following command:
|
||||
|
||||
```bash
|
||||
npm run preflight
|
||||
```
|
||||
|
||||
This single command ensures that your changes meet all the quality gates of the project. While you can run the individual steps (`build`, `test`, `typecheck`, `lint`) separately, it is highly recommended to use `npm run preflight` to ensure a comprehensive validation.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
This project uses **Vitest** as its primary testing framework. When writing tests, aim to follow existing patterns. Key conventions include:
|
||||
|
||||
### Test Structure and Framework
|
||||
|
||||
- **Framework**: All tests are written using Vitest (`describe`, `it`, `expect`, `vi`).
|
||||
- **File Location**: Test files (`*.test.ts` for logic, `*.test.tsx` for React components) are co-located with the source files they test.
|
||||
- **Configuration**: Test environments are defined in `vitest.config.ts` files.
|
||||
- **Setup/Teardown**: Use `beforeEach` and `afterEach`. Commonly, `vi.resetAllMocks()` is called in `beforeEach` and `vi.restoreAllMocks()` in `afterEach`.
|
||||
|
||||
### Mocking (`vi` from Vitest)
|
||||
|
||||
- **ES Modules**: Mock with `vi.mock('module-name', async (importOriginal) => { ... })`. Use `importOriginal` for selective mocking.
|
||||
- _Example_: `vi.mock('os', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, homedir: vi.fn() }; });`
|
||||
- **Mocking Order**: For critical dependencies (e.g., `os`, `fs`) that affect module-level constants, place `vi.mock` at the _very top_ of the test file, before other imports.
|
||||
- **Hoisting**: Use `const myMock = vi.hoisted(() => vi.fn());` if a mock function needs to be defined before its use in a `vi.mock` factory.
|
||||
- **Mock Functions**: Create with `vi.fn()`. Define behavior with `mockImplementation()`, `mockResolvedValue()`, or `mockRejectedValue()`.
|
||||
- **Spying**: Use `vi.spyOn(object, 'methodName')`. Restore spies with `mockRestore()` in `afterEach`.
|
||||
|
||||
### Commonly Mocked Modules
|
||||
|
||||
- **Node.js built-ins**: `fs`, `fs/promises`, `os` (especially `os.homedir()`), `path`, `child_process` (`execSync`, `spawn`).
|
||||
- **External SDKs**: `@google/genai`, `@modelcontextprotocol/sdk`.
|
||||
- **Internal Project Modules**: Dependencies from other project packages are often mocked.
|
||||
|
||||
### React Component Testing (CLI UI - Ink)
|
||||
|
||||
- Use `render()` from `ink-testing-library`.
|
||||
- Assert output with `lastFrame()`.
|
||||
- Wrap components in necessary `Context.Provider`s.
|
||||
- Mock custom React hooks and complex child components using `vi.mock()`.
|
||||
|
||||
### Asynchronous Testing
|
||||
|
||||
- Use `async/await`.
|
||||
- For timers, use `vi.useFakeTimers()`, `vi.advanceTimersByTimeAsync()`, `vi.runAllTimersAsync()`.
|
||||
- Test promise rejections with `await expect(promise).rejects.toThrow(...)`.
|
||||
|
||||
### General Guidance
|
||||
|
||||
- When adding tests, first examine existing tests to understand and conform to established conventions.
|
||||
- Pay close attention to the mocks at the top of existing test files; they reveal critical dependencies and how they are managed in a test environment.
|
||||
|
||||
## Git Repo
|
||||
|
||||
The main branch for this project is called "main"
|
||||
|
||||
## JavaScript/TypeScript
|
||||
|
||||
When contributing to this React, Node, and TypeScript codebase, please prioritize the use of plain JavaScript objects with accompanying TypeScript interface or type declarations over JavaScript class syntax. This approach offers significant advantages, especially concerning interoperability with React and overall code maintainability.
|
||||
|
||||
### Preferring Plain Objects over Classes
|
||||
|
||||
JavaScript classes, by their nature, are designed to encapsulate internal state and behavior. While this can be useful in some object-oriented paradigms, it often introduces unnecessary complexity and friction when working with React's component-based architecture. Here's why plain objects are preferred:
|
||||
|
||||
- Seamless React Integration: React components thrive on explicit props and state management. Classes' tendency to store internal state directly within instances can make prop and state propagation harder to reason about and maintain. Plain objects, on the other hand, are inherently immutable (when used thoughtfully) and can be easily passed as props, simplifying data flow and reducing unexpected side effects.
|
||||
|
||||
- Reduced Boilerplate and Increased Conciseness: Classes often promote the use of constructors, this binding, getters, setters, and other boilerplate that can unnecessarily bloat code. TypeScript interface and type declarations provide powerful static type checking without the runtime overhead or verbosity of class definitions. This allows for more succinct and readable code, aligning with JavaScript's strengths in functional programming.
|
||||
|
||||
- Enhanced Readability and Predictability: Plain objects, especially when their structure is clearly defined by TypeScript interfaces, are often easier to read and understand. Their properties are directly accessible, and there's no hidden internal state or complex inheritance chains to navigate. This predictability leads to fewer bugs and a more maintainable codebase.
|
||||
Simplified Immutability: While not strictly enforced, plain objects encourage an immutable approach to data. When you need to modify an object, you typically create a new one with the desired changes, rather than mutating the original. This pattern aligns perfectly with React's reconciliation process and helps prevent subtle bugs related to shared mutable state.
|
||||
|
||||
- Better Serialization and Deserialization: Plain JavaScript objects are naturally easy to serialize to JSON and deserialize back, which is a common requirement in web development (e.g., for API communication or local storage). Classes, with their methods and prototypes, can complicate this process.
|
||||
|
||||
### Embracing ES Module Syntax for Encapsulation
|
||||
|
||||
Rather than relying on Java-esque private or public class members, which can be verbose and sometimes limit flexibility, we strongly prefer leveraging ES module syntax (`import`/`export`) for encapsulating private and public APIs.
|
||||
|
||||
- Clearer Public API Definition: With ES modules, anything that is exported is part of the public API of that module, while anything not exported is inherently private to that module. This provides a very clear and explicit way to define what parts of your code are meant to be consumed by other modules.
|
||||
|
||||
- Enhanced Testability (Without Exposing Internals): By default, unexported functions or variables are not accessible from outside the module. This encourages you to test the public API of your modules, rather than their internal implementation details. If you find yourself needing to spy on or stub an unexported function for testing purposes, it's often a "code smell" indicating that the function might be a good candidate for extraction into its own separate, testable module with a well-defined public API. This promotes a more robust and maintainable testing strategy.
|
||||
|
||||
- Reduced Coupling: Explicitly defined module boundaries through import/export help reduce coupling between different parts of your codebase. This makes it easier to refactor, debug, and understand individual components in isolation.
|
||||
|
||||
### Avoiding `any` Types and Type Assertions; Preferring `unknown`
|
||||
|
||||
TypeScript's power lies in its ability to provide static type checking, catching potential errors before your code runs. To fully leverage this, it's crucial to avoid the `any` type and be judicious with type assertions.
|
||||
|
||||
- **The Dangers of `any`**: Using any effectively opts out of TypeScript's type checking for that particular variable or expression. While it might seem convenient in the short term, it introduces significant risks:
|
||||
|
||||
- **Loss of Type Safety**: You lose all the benefits of type checking, making it easy to introduce runtime errors that TypeScript would otherwise have caught.
|
||||
- **Reduced Readability and Maintainability**: Code with `any` types is harder to understand and maintain, as the expected type of data is no longer explicitly defined.
|
||||
- **Masking Underlying Issues**: Often, the need for any indicates a deeper problem in the design of your code or the way you're interacting with external libraries. It's a sign that you might need to refine your types or refactor your code.
|
||||
|
||||
- **Preferring `unknown` over `any`**: When you absolutely cannot determine the type of a value at compile time, and you're tempted to reach for any, consider using unknown instead. unknown is a type-safe counterpart to any. While a variable of type unknown can hold any value, you must perform type narrowing (e.g., using typeof or instanceof checks, or a type assertion) before you can perform any operations on it. This forces you to handle the unknown type explicitly, preventing accidental runtime errors.
|
||||
|
||||
```
|
||||
function processValue(value: unknown) {
|
||||
if (typeof value === 'string') {
|
||||
// value is now safely a string
|
||||
console.log(value.toUpperCase());
|
||||
} else if (typeof value === 'number') {
|
||||
// value is now safely a number
|
||||
console.log(value * 2);
|
||||
}
|
||||
// Without narrowing, you cannot access properties or methods on 'value'
|
||||
// console.log(value.someProperty); // Error: Object is of type 'unknown'.
|
||||
}
|
||||
```
|
||||
|
||||
- **Type Assertions (`as Type`) - Use with Caution**: Type assertions tell the TypeScript compiler, "Trust me, I know what I'm doing; this is definitely of this type." While there are legitimate use cases (e.g., when dealing with external libraries that don't have perfect type definitions, or when you have more information than the compiler), they should be used sparingly and with extreme caution.
|
||||
- **Bypassing Type Checking**: Like `any`, type assertions bypass TypeScript's safety checks. If your assertion is incorrect, you introduce a runtime error that TypeScript would not have warned you about.
|
||||
- **Code Smell in Testing**: A common scenario where `any` or type assertions might be tempting is when trying to test "private" implementation details (e.g., spying on or stubbing an unexported function within a module). This is a strong indication of a "code smell" in your testing strategy and potentially your code structure. Instead of trying to force access to private internals, consider whether those internal details should be refactored into a separate module with a well-defined public API. This makes them inherently testable without compromising encapsulation.
|
||||
|
||||
### Embracing JavaScript's Array Operators
|
||||
|
||||
To further enhance code cleanliness and promote safe functional programming practices, leverage JavaScript's rich set of array operators as much as possible. Methods like `.map()`, `.filter()`, `.reduce()`, `.slice()`, `.sort()`, and others are incredibly powerful for transforming and manipulating data collections in an immutable and declarative way.
|
||||
|
||||
Using these operators:
|
||||
|
||||
- Promotes Immutability: Most array operators return new arrays, leaving the original array untouched. This functional approach helps prevent unintended side effects and makes your code more predictable.
|
||||
- Improves Readability: Chaining array operators often leads to more concise and expressive code than traditional for loops or imperative logic. The intent of the operation is clear at a glance.
|
||||
- Facilitates Functional Programming: These operators are cornerstones of functional programming, encouraging the creation of pure functions that take inputs and produce outputs without causing side effects. This paradigm is highly beneficial for writing robust and testable code that pairs well with React.
|
||||
|
||||
By consistently applying these principles, we can maintain a codebase that is not only efficient and performant but also a joy to work with, both now and in the future.
|
||||
|
||||
## React (mirrored and adjusted from [react-mcp-server](https://github.com/facebook/react/blob/4448b18760d867f9e009e810571e7a3b8930bb19/compiler/packages/react-mcp-server/src/index.ts#L376C1-L441C94))
|
||||
|
||||
### Role
|
||||
|
||||
You are a React assistant that helps users write more efficient and optimizable React code. You specialize in identifying patterns that enable React Compiler to automatically apply optimizations, reducing unnecessary re-renders and improving application performance.
|
||||
|
||||
### Follow these guidelines in all code you produce and suggest
|
||||
|
||||
Use functional components with Hooks: Do not generate class components or use old lifecycle methods. Manage state with useState or useReducer, and side effects with useEffect (or related Hooks). Always prefer functions and Hooks for any new component logic.
|
||||
|
||||
Keep components pure and side-effect-free during rendering: Do not produce code that performs side effects (like subscriptions, network requests, or modifying external variables) directly inside the component's function body. Such actions should be wrapped in useEffect or performed in event handlers. Ensure your render logic is a pure function of props and state.
|
||||
|
||||
Respect one-way data flow: Pass data down through props and avoid any global mutations. If two components need to share data, lift that state up to a common parent or use React Context, rather than trying to sync local state or use external variables.
|
||||
|
||||
Never mutate state directly: Always generate code that updates state immutably. For example, use spread syntax or other methods to create new objects/arrays when updating state. Do not use assignments like state.someValue = ... or array mutations like array.push() on state variables. Use the state setter (setState from useState, etc.) to update state.
|
||||
|
||||
Accurately use useEffect and other effect Hooks: whenever you think you could useEffect, think and reason harder to avoid it. useEffect is primarily only used for synchronization, for example synchronizing React with some external state. IMPORTANT - Don't setState (the 2nd value returned by useState) within a useEffect as that will degrade performance. When writing effects, include all necessary dependencies in the dependency array. Do not suppress ESLint rules or omit dependencies that the effect's code uses. Structure the effect callbacks to handle changing values properly (e.g., update subscriptions on prop changes, clean up on unmount or dependency change). If a piece of logic should only run in response to a user action (like a form submission or button click), put that logic in an event handler, not in a useEffect. Where possible, useEffects should return a cleanup function.
|
||||
|
||||
Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect, useContext, custom Hooks, etc.) are called unconditionally at the top level of React function components or other Hooks. Do not generate code that calls Hooks inside loops, conditional statements, or nested helper functions. Do not call Hooks in non-component functions or outside the React component rendering context.
|
||||
|
||||
Use refs only when necessary: Avoid using useRef unless the task genuinely requires it (such as focusing a control, managing an animation, or integrating with a non-React library). Do not use refs to store application state that should be reactive. If you do use refs, never write to or read from ref.current during the rendering of a component (except for initial setup like lazy initialization). Any ref usage should not affect the rendered output directly.
|
||||
|
||||
Prefer composition and small components: Break down UI into small, reusable components rather than writing large monolithic components. The code you generate should promote clarity and reusability by composing components together. Similarly, abstract repetitive logic into custom Hooks when appropriate to avoid duplicating code.
|
||||
|
||||
Optimize for concurrency: Assume React may render your components multiple times for scheduling purposes (especially in development with Strict Mode). Write code that remains correct even if the component function runs more than once. For instance, avoid side effects in the component body and use functional state updates (e.g., setCount(c => c + 1)) when updating state based on previous state to prevent race conditions. Always include cleanup functions in effects that subscribe to external resources. Don't write useEffects for "do this when this changes" side-effects. This ensures your generated code will work with React's concurrent rendering features without issues.
|
||||
|
||||
Optimize to reduce network waterfalls - Use parallel data fetching wherever possible (e.g., start multiple requests at once rather than one after another). Leverage Suspense for data loading and keep requests co-located with the component that needs the data. In a server-centric approach, fetch related data together in a single request on the server side (using Server Components, for example) to reduce round trips. Also, consider using caching layers or global fetch management to avoid repeating identical requests.
|
||||
|
||||
Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if React Compiler is enabled. Avoid premature optimization with manual memoization. Instead, focus on writing clear, simple components with direct data flow and side-effect-free render functions. Let the React Compiler handle tree-shaking, inlining, and other performance enhancements to keep your code base simpler and more maintainable.
|
||||
|
||||
Design for a good user experience - Provide clear, minimal, and non-blocking UI states. When data is loading, show lightweight placeholders (e.g., skeleton screens) rather than intrusive spinners everywhere. Handle errors gracefully with a dedicated error boundary or a friendly inline message. Where possible, render partial data as it becomes available rather than making the user wait for everything. Suspense allows you to declare the loading states in your component tree in a natural way, preventing “flash” states and improving perceived performance.
|
||||
|
||||
### Process
|
||||
|
||||
1. Analyze the user's code for optimization opportunities:
|
||||
|
||||
- Check for React anti-patterns that prevent compiler optimization
|
||||
- Look for component structure issues that limit compiler effectiveness
|
||||
- Think about each suggestion you are making and consult React docs for best practices
|
||||
|
||||
2. Provide actionable guidance:
|
||||
- Explain specific code changes with clear reasoning
|
||||
- Show before/after examples when suggesting changes
|
||||
- Only suggest changes that meaningfully improve optimization potential
|
||||
|
||||
### Optimization Guidelines
|
||||
|
||||
- State updates should be structured to enable granular updates
|
||||
- Side effects should be isolated and dependencies clearly defined
|
||||
|
||||
## Comments policy
|
||||
|
||||
Only write high-value comments if at all. Avoid talking to the user through comments.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Makefile for gemini-cli
|
||||
|
||||
.PHONY: help install build build-sandbox build-all test lint format preflight clean start debug release run-npx create-alias
|
||||
|
||||
help:
|
||||
@echo "Makefile for gemini-cli"
|
||||
@echo ""
|
||||
@echo "Usage:"
|
||||
@echo " make install - Install npm dependencies"
|
||||
@echo " make build - Build the entire project"
|
||||
@echo " make build-sandbox - Build the sandbox container"
|
||||
@echo " make build-all - Build the project and the sandbox"
|
||||
@echo " make test - Run the test suite"
|
||||
@echo " make lint - Lint the code"
|
||||
@echo " make format - Format the code"
|
||||
@echo " make preflight - Run formatting, linting, and tests"
|
||||
@echo " make clean - Remove generated files"
|
||||
@echo " make start - Start the Gemini CLI"
|
||||
@echo " make debug - Start the Gemini CLI in debug mode"
|
||||
@echo " make release - Publish a new release"
|
||||
@echo " make run-npx - Run the CLI using npx (for testing the published package)"
|
||||
@echo " make create-alias - Create a 'gemini' alias for your shell"
|
||||
|
||||
install:
|
||||
npm install
|
||||
|
||||
build:
|
||||
npm run build
|
||||
|
||||
build-sandbox:
|
||||
npm run build:sandbox
|
||||
|
||||
build-all:
|
||||
npm run build:all
|
||||
|
||||
test:
|
||||
npm run test
|
||||
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
format:
|
||||
npm run format
|
||||
|
||||
preflight:
|
||||
npm run preflight
|
||||
|
||||
clean:
|
||||
npm run clean
|
||||
|
||||
start:
|
||||
npm run start
|
||||
|
||||
debug:
|
||||
npm run debug
|
||||
|
||||
release:
|
||||
npm run publish:release
|
||||
|
||||
run-npx:
|
||||
npx https://github.com/google-gemini/gemini-cli
|
||||
|
||||
create-alias:
|
||||
scripts/create_alias.sh
|
||||
@@ -1,49 +1,121 @@
|
||||
# Gemini Code
|
||||
# Gemini CLI
|
||||
|
||||
**Disclaimer:** This README.md was created by gemini-code and this project was developed rapidly and currently lacks comprehensive testing, CI/CD pipelines, and other quality-of-life features common in mature projects.
|
||||
[](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml)
|
||||
|
||||
This repository contains the Gemini Code CLI tool.
|
||||
This repository contains the Gemini CLI tool.
|
||||
|
||||
## Cloning and Contribution
|
||||
## Quickstart
|
||||
|
||||
This project uses Gerrit for version control. For details on the development workflow, including how to clone the repository and contribute changes, please refer to the [Gerrit Dev Workflows](http://go/gerrit-dev-workflows).
|
||||
1. [Install Node 18+](https://nodejs.org/en/download).
|
||||
2. [Get an API key from Google AI Studio](https://aistudio.google.com/apikey).
|
||||
3. Set the API key in your shell using the following command, replacing `YOUR_API_KEY` with the API key you obtained: `export GEMINI_API_KEY="YOUR_API_KEY"`.
|
||||
4. Run the Gemini CLI from your shell using the following command: `npx https://github.com/google-gemini/gemini-cli`
|
||||
5. Enjoy.
|
||||
|
||||
## Building
|
||||
## Examples
|
||||
|
||||
To build the entire project, including the CLI package, run the following command from the root directory:
|
||||
Once the CLI is running, you can start interacting with Gemini from your shell. Try a simple query:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
> How can I build a web app?
|
||||
```
|
||||
|
||||
This command installs dependencies and then runs the build script defined in the root `package.json`, which in turn executes the build scripts in all workspaces (including `packages/cli`).
|
||||
Or ask it to perform a task using its tools:
|
||||
|
||||
## Running
|
||||
|
||||
To start the Gemini Code CLI, run the following command from the root directory:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
> List files in the current directory.
|
||||
```
|
||||
|
||||
This command executes the `start` script defined in the root `package.json`, which specifically targets and runs the `start` script within the `gemini-code-cli` workspace.
|
||||
## API Key Setup
|
||||
|
||||
## Debugging
|
||||
The Gemini CLI requires you to authenticate with Google's AI services. You'll need to configure **one** of the following authentication methods:
|
||||
|
||||
To debug the CLI application using VS Code:
|
||||
1. **Gemini Code Assist:**
|
||||
|
||||
1. Start the CLI in debug mode from the root directory:
|
||||
```bash
|
||||
npm run debug --workspace=gemini-code-cli
|
||||
```
|
||||
This command runs `node --inspect-brk dist/gemini.js` within the `packages/cli` directory, pausing execution until a debugger attaches.
|
||||
2. In VS Code, use the "Attach" launch configuration (found in `.vscode/launch.json`). This configuration is set up to attach to the Node.js process listening on port 9229, which is the default port used by `--inspect-brk`.
|
||||
- To enable this mode you only need set the GEMINI_CODE_ASSIST environment variable to true.
|
||||
- You can temporarily set the environment variable in your current shell session using the following command:
|
||||
```bash
|
||||
export GEMINI_CODE_ASSIST="true"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GEMINI_CODE_ASSIST="true"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
- There are two types of Google Accounts you can use with Gemini CLI:
|
||||
- **Personal Google Account**: This is the standard, free account you use for services like Gmail, Google Photos, and Google Drive for personal use (e.g. your-name@gmail.com).
|
||||
- **Google Workspace Account**: This is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school.
|
||||
- Google Workspace Account must configure a Google Cloud Project Id to use. You can temporarily set the environment variable in your current shell session using the following command:
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT_ID="YOUR_PROJECT_ID"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT_ID="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
- During start up, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs. Cached credentials last about 20 hours before expiring.
|
||||
- Note that the the web login must be done in a browser that can communicate with the machine Gemini Cli is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on).
|
||||
|
||||
Alternatively, you can use the "Launch Program" configuration in VS Code if you prefer to launch the currently open file directly, but the "Attach" method is generally recommended for debugging the main CLI entry point.
|
||||
2. **Gemini API key:**
|
||||
|
||||
## Related
|
||||
- Obtain your API key from Google AI Studio: [https://aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
|
||||
- Set the `GEMINI_API_KEY` environment variable. In the following methods, replace `YOUR_GEMINI_API_KEY` with the API key you obtained from Google AI Studio:
|
||||
- You can temporarily set the environment variable in your current shell session using the following command:
|
||||
```bash
|
||||
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
- [Document](http://go/gemini-code-cli)
|
||||
- [Video Walkthrough](https://screencast.googleplex.com/cast/NDkwMjUwMzMxMjI2MTEyMHwwOWZkMjQzYy03Mw)
|
||||
3. **Google API Key (Vertex AI Express Mode):**
|
||||
|
||||
- You can use a general Google Cloud API key if it has been enabled for the Gemini API or Vertex AI.
|
||||
- Set the `GOOGLE_API_KEY` and `GOOGLE_GENAI_USE_VERTEXAI` environment variables. In the following methods, replace `YOUR_GEMINI_API_KEY` with your Google Cloud API key:
|
||||
- You can temporarily set these environment variables in your current shell session using the following commands:
|
||||
```bash
|
||||
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
|
||||
export GOOGLE_GENAI_USE_VERTEXAI=true
|
||||
```
|
||||
- For repeated use, you can add the environment variables to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands adds the environment variables to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_GENAI_USE_VERTEXAI=true' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
4. **Vertex AI (Project and Location):**
|
||||
- Ensure you have a Google Cloud project and have enabled the Vertex AI API.
|
||||
- Set up Application Default Credentials (ADC), using the following command:
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
For more information, see [Set up Application Default Credentials for Google Cloud](https://cloud.google.com/docs/authentication/provide-credentials-adc).
|
||||
- Set the `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_GENAI_USE_VERTEXAI` environment variables. In the following methods, replace `YOUR_PROJECT_ID` and `YOUR_PROJECT_LOCATION` with the relevant values for your project:
|
||||
- You can temporarily set these environment variables in your current shell session using the following commands:
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION" # e.g., us-central1
|
||||
export GOOGLE_GENAI_USE_VERTEXAI=true
|
||||
```
|
||||
- For repeated use, you can add the environment variables to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands adds the environment variables to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_GENAI_USE_VERTEXAI=true' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Learn how to [contribute to or build from the source](./CONTRIBUTING.md).
|
||||
- Explore the available **[CLI Commands](./docs/cli/commands.md)**.
|
||||
- If you encounter any issues, review the **[Troubleshooting guide](./docs/troubleshooting.md)**.
|
||||
- For more comprehensive documentation, see the [full documentation](./docs/index.md).
|
||||
|
||||
## Gemini APIs
|
||||
|
||||
This project leverages the Gemini APIs to provide AI capabilities. For details on the terms of service governing the Gemini API, please refer to the [Gemini API Terms of Service](https://ai.google.dev/gemini-api/terms).
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Gemini CLI Architecture Overview
|
||||
|
||||
This document provides a high-level overview of the Gemini CLI's architecture.
|
||||
|
||||
## Core components
|
||||
|
||||
The Gemini CLI is primarily composed of two main packages, along with a suite of tools that can be used by the system in the course of handling command-line input:
|
||||
|
||||
1. **CLI package (`packages/cli`):**
|
||||
|
||||
- **Purpose:** This contains the user-facing portion of the Gemini CLI, such as handling the initial user input, presenting the final output, and managing the overall user experience.
|
||||
- **Key functions contained in the package:**
|
||||
- [Input processing](./cli/commands.md)
|
||||
- History management
|
||||
- Display rendering
|
||||
- [Theme and UI customization](./cli/themes.md)
|
||||
- [CLI configuration settings](./cli/configuration.md)
|
||||
|
||||
2. **Core package (`packages/core`):**
|
||||
|
||||
- **Purpose:** This acts as the backend for the Gemini CLI. It receives requests sent from `packages/cli`, orchestrates interactions with the Gemini API, and manages the execution of available tools.
|
||||
- **Key functions contained in the package:**
|
||||
- API client for communicating with the Google Gemini API
|
||||
- Prompt construction and management
|
||||
- Tool registration and execution logic
|
||||
- State management for conversations or sessions
|
||||
- Server-side configuration
|
||||
|
||||
3. **Tools (`packages/core/src/tools/`):**
|
||||
- **Purpose:** These are individual modules that extend the capabilities of the Gemini model, allowing it to interact with the local environment (e.g., file system, shell commands, web fetching).
|
||||
- **Interaction:** `packages/core` invokes these tools based on requests from the Gemini model.
|
||||
|
||||
## Interaction Flow
|
||||
|
||||
A typical interaction with the Gemini CLI follows this flow:
|
||||
|
||||
1. **User input:** The user types a prompt or command into the terminal, which is managed by `packages/cli`.
|
||||
2. **Request to core:** `packages/cli` sends the user's input to `packages/core`.
|
||||
3. **Request processed:** The core package:
|
||||
- Constructs an appropriate prompt for the Gemini API, possibly including conversation history and available tool definitions.
|
||||
- Sends the prompt to the Gemini API.
|
||||
4. **Gemini API response:** The Gemini API processes the prompt and returns a response. This response might be a direct answer or a request to use one of the available tools.
|
||||
5. **Tool execution (if applicable):**
|
||||
- When the Gemini API requests a tool, the core package prepares to execute it.
|
||||
- If the requested tool can modify the file system or execute shell commands, the user is first given details of the tool and its arguments, and the user must approve the execution.
|
||||
- Read-only operations, such as reading files, might not require explicit user confirmation to proceed.
|
||||
- Once confirmed, or if confirmation is not required, the core package executes the relevant action within the relevant tool, and the result is sent back to the Gemini API by the core package.
|
||||
- The Gemini API processes the tool result and generates a final response.
|
||||
6. **Response to CLI:** The core package sends the final response back to the CLI package.
|
||||
7. **Display to user:** The CLI package formats and displays the response to the user in the terminal.
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
- **Modularity:** Separating the CLI (frontend) from the Core (backend) allows for independent development and potential future extensions (e.g., different frontends for the same backend).
|
||||
- **Extensibility:** The tool system is designed to be extensible, allowing new capabilities to be added.
|
||||
- **User experience:** The CLI focuses on providing a rich and interactive terminal experience.
|
||||
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 125 KiB |
@@ -0,0 +1,121 @@
|
||||
# CLI Commands
|
||||
|
||||
Gemini CLI supports several built-in commands to help you manage your session, customize the interface, and control its behavior. These commands are prefixed with a forward slash (`/`), an at symbol (`@`), or an exclamation mark (`!`).
|
||||
|
||||
## Slash commands (`/`)
|
||||
|
||||
Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
- **`/bug`**
|
||||
|
||||
- **Description:** File an issue about Gemini CLI. By default, the issue is filed within the GitHub repository for Gemini CLI. The string you enter after `/bug` will become the headline for the bug being filed. The default `/bug` behavior can be modified using the `bugCommand` setting in your `.gemini/settings.json` files.
|
||||
|
||||
- **`/clear`**
|
||||
|
||||
- **Description:** Clear the terminal screen, including the visible session history and scrollback within the CLI. The underlying session data (for history recall) might be preserved depending on the exact implementation, but the visual display is cleared.
|
||||
- **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action.
|
||||
|
||||
- **`/compress`**
|
||||
|
||||
- **Description:** Replace the entire chat context with a summary. This saves on tokens used for future tasks while retaining a high level summary of what has happened.
|
||||
|
||||
- **`/editor`**
|
||||
|
||||
- **Description:** Open a dialog for selecting supported editors.
|
||||
|
||||
- **`/help`** (or **`/?`**)
|
||||
|
||||
- **Description:** Display help information about the Gemini CLI, including available commands and their usage.
|
||||
|
||||
- **`/mcp`**
|
||||
|
||||
- **Description:** List configured Model Context Protocol (MCP) servers, their connection status, server details, and available tools.
|
||||
- **Sub-commands:**
|
||||
- **`desc`** or **`descriptions`**:
|
||||
- **Description:** Show detailed descriptions for MCP servers and tools.
|
||||
- **`nodesc`** or **`nodescriptions`**:
|
||||
- **Description:** Hide tool descriptions, showing only the tool names.
|
||||
- **`schema`**:
|
||||
- **Description:** Show the full JSON schema for the tool's configured parameters.
|
||||
- **Keyboard Shortcut:** Press **Ctrl+T** at any time to toggle between showing and hiding tool descriptions.
|
||||
|
||||
- **`/memory`**
|
||||
|
||||
- **Description:** Manage the AI's instructional context (hierarchical memory loaded from `GEMINI.md` files).
|
||||
- **Sub-commands:**
|
||||
- **`show`**:
|
||||
- **Description:** Display the full, concatenated content of the current hierarchical memory that has been loaded from all `GEMINI.md` files. This lets you inspect the instructional context being provided to the Gemini model.
|
||||
- **`refresh`**:
|
||||
- **Description:** Reload the hierarchical instructional memory from all `GEMINI.md` files found in the configured locations (global, project/ancestors, and sub-directories). This command updates the model with the latest `GEMINI.md` content.
|
||||
- **Note:** For more details on how `GEMINI.md` files contribute to hierarchical memory, see the [CLI Configuration documentation](./configuration.md#4-geminimd-files-hierarchical-instructional-context).
|
||||
|
||||
- **`/stats`**
|
||||
|
||||
- **Description:** Display detailed statistics for the current Gemini CLI session, such as the session duration.
|
||||
|
||||
- [**`/theme`**](./themes.md)
|
||||
|
||||
- **Description:** Open a dialog that lets you change the visual theme of Gemini CLI.
|
||||
|
||||
- [**`/tools`**](../tools/index.md)
|
||||
|
||||
- **Description:** Display a list of tools that are currently available within Gemini CLI.
|
||||
- **Sub-commands:**
|
||||
- **`desc`** or **`descriptions`**:
|
||||
- **Description:** Show detailed descriptions of each tool, including each tool's name with its full description as provided to the model.
|
||||
- **`nodesc`** or **`nodescriptions`**:
|
||||
- **Description:** Hide tool descriptions, showing only the tool names.
|
||||
|
||||
- **`/quit`** (or **`/exit`**)
|
||||
|
||||
- **Description:** Exit Gemini CLI.
|
||||
|
||||
## At commands (`@`)
|
||||
|
||||
At commands are used to include the content of files or directories as part of your prompt to Gemini. These commands include git-aware filtering.
|
||||
|
||||
- **`@<path_to_file_or_directory>`**
|
||||
|
||||
- **Description:** Inject the content of the specified file or files into your current prompt. This is useful for asking questions about specific code, text, or collections of files.
|
||||
- **Examples:**
|
||||
- `@path/to/your/file.txt Explain this text.`
|
||||
- `@src/my_project/ Summarize the code in this directory.`
|
||||
- `What is this file about? @README.md`
|
||||
- **Details:**
|
||||
- If a path to a single file is provided, the content of that file is read.
|
||||
- If a path to a directory is provided, the command attempts to read the content of files within that directory and any subdirectories.
|
||||
- Spaces in paths should be escaped with a backslash (e.g., `@My\ Documents/file.txt`).
|
||||
- The command uses the `read_many_files` tool internally. The content is fetched and then inserted into your query before being sent to the Gemini model.
|
||||
- **Git-aware filtering:** By default, git-ignored files (like `node_modules/`, `dist/`, `.env`, `.git/`) are excluded. This behavior can be changed via the `fileFiltering` settings.
|
||||
- **File types:** The command is intended for text-based files. While it might attempt to read any file, binary files or very large files might be skipped or truncated by the underlying `read_many_files` tool to ensure performance and relevance. The tool indicates if files were skipped.
|
||||
- **Output:** The CLI will show a tool call message indicating that `read_many_files` was used, along with a message detailing the status and the path(s) that were processed.
|
||||
|
||||
- **`@` (Lone at symbol)**
|
||||
- **Description:** If you type a lone `@` symbol without a path, the query is passed as-is to the Gemini model. This might be useful if you are specifically talking _about_ the `@` symbol in your prompt.
|
||||
|
||||
### Error handling for `@` commands
|
||||
|
||||
- If the path specified after `@` is not found or is invalid, an error message will be displayed, and the query might not be sent to the Gemini model, or it will be sent without the file content.
|
||||
- If the `read_many_files` tool encounters an error (e.g., permission issues), this will also be reported.
|
||||
|
||||
## Shell mode & passthrough commands (`!`)
|
||||
|
||||
The `!` prefix lets you interact with your system's shell directly from within Gemini CLI.
|
||||
|
||||
- **`!<shell_command>`**
|
||||
|
||||
- **Description:** Execute the given `<shell_command>` in your system's default shell. Any output or errors from the command are displayed in the terminal.
|
||||
- **Examples:**
|
||||
- `!ls -la` (executes `ls -la` and returns to Gemini CLI)
|
||||
- `!git status` (executes `git status` and returns to Gemini CLI)
|
||||
|
||||
- **`!` (Toggle shell mode)**
|
||||
|
||||
- **Description:** Typing `!` on its own toggles shell mode.
|
||||
- **Entering shell mode:**
|
||||
- When active, shell mode uses a different coloring and a "Shell Mode Indicator".
|
||||
- While in shell mode, text you type is interpreted directly as a shell command.
|
||||
- **Exiting shell mode:**
|
||||
- When exited, the UI reverts to its standard appearance and normal Gemini CLI behavior resumes.
|
||||
|
||||
- **Caution for all `!` usage:** Commands you execute in shell mode have the same permissions and impact as if you ran them directly in your terminal.
|
||||
@@ -0,0 +1,337 @@
|
||||
# Gemini CLI Configuration
|
||||
|
||||
Gemini CLI offers several ways to configure its behavior, including environment variables, command-line arguments, and settings files. This document outlines the different configuration methods and available settings.
|
||||
|
||||
## Configuration layers
|
||||
|
||||
Configuration is applied in the following order of precedence (lower numbers are overridden by higher numbers):
|
||||
|
||||
1. **Default values:** Hardcoded defaults within the application.
|
||||
2. **User settings file:** Global settings for the current user.
|
||||
3. **Project settings file:** Project-specific settings.
|
||||
4. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files.
|
||||
5. **Command-line arguments:** Values passed when launching the CLI.
|
||||
|
||||
## The user settings file and project settings file
|
||||
|
||||
Gemini CLI uses `settings.json` files for persistent configuration. There are two locations for these files:
|
||||
|
||||
- **User settings file:**
|
||||
- **Location:** `~/.gemini/settings.json` (where `~` is your home directory).
|
||||
- **Scope:** Applies to all Gemini CLI sessions for the current user.
|
||||
- **Project settings file:**
|
||||
- **Location:** `.gemini/settings.json` within your project's root directory.
|
||||
- **Scope:** Applies only when running Gemini CLI from that specific project. Project settings override user settings.
|
||||
|
||||
**Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`.
|
||||
|
||||
### The `.gemini` directory in your project
|
||||
|
||||
In addition to a project settings file, a project's `.gemini` directory can contain other project-specific files related to Gemini CLI's operation, such as:
|
||||
|
||||
- [Custom sandbox profiles](#sandboxing) (e.g., `.gemini/sandbox-macos-custom.sb`, `.gemini/sandbox.Dockerfile`).
|
||||
- [Telemtry configurations](../core/telemetry.md#running-an-otel-collector) (e.g., `.gemini/otel/`).
|
||||
|
||||
### Available settings in `settings.json`:
|
||||
|
||||
- **`contextFileName`** (string or array of strings):
|
||||
|
||||
- **Description:** Specifies the filename for context files (e.g., `GEMINI.md`, `AGENTS.md`). Can be a single filename or a list of accepted filenames.
|
||||
- **Default:** `GEMINI.md`
|
||||
- **Example:** `"contextFileName": "AGENTS.md"`
|
||||
|
||||
- **`bugCommand`** (object):
|
||||
|
||||
- **Description:** Overrides the default URL for the `/bug` command.
|
||||
- **Default:** `"urlTemplate": "https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.md&title={title}&body={body}"`
|
||||
- **Properties:**
|
||||
- **`urlTemplate`** (string): A URL that can contain `{title}` and `{body}` placeholders.
|
||||
- **Example:**
|
||||
```json
|
||||
"bugCommand": {
|
||||
"urlTemplate": "https://bug.example.com/new?title={title}&body={body}"
|
||||
}
|
||||
```
|
||||
|
||||
- **`fileFiltering`** (object):
|
||||
|
||||
- **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools.
|
||||
- **Default:** `"respectGitIgnore": true`
|
||||
- **Properties:**
|
||||
- **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations.
|
||||
- **Example:**
|
||||
```json
|
||||
"fileFiltering": {
|
||||
"respectGitIgnore": true
|
||||
}
|
||||
```
|
||||
|
||||
- **`coreTools`** (array of strings):
|
||||
|
||||
- **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools.
|
||||
- **Default:** All tools available for use by the Gemini model.
|
||||
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "SearchText"]`.
|
||||
|
||||
- **`excludeTools`** (array of strings):
|
||||
|
||||
- **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded.
|
||||
- **Default**: No tools excluded.
|
||||
- **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
|
||||
|
||||
- **`autoAccept`** (boolean):
|
||||
|
||||
- **Description:** Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe.
|
||||
- **Default:** `false`
|
||||
- **Example:** `"autoAccept": true`
|
||||
|
||||
- **`theme`** (string):
|
||||
|
||||
- **Description:** Sets the visual [theme](./themes.md) for Gemini CLI.
|
||||
- **Default:** `"Default"`
|
||||
- **Example:** `"theme": "GitHub"`
|
||||
|
||||
- **`sandbox`** (boolean or string):
|
||||
|
||||
- **Description:** Controls whether and how to use sandboxing for tool execution. If set to `true`, Gemini CLI uses a pre-built `gemini-cli-sandbox` Docker image. For more information, see [Sandboxing](#sandboxing).
|
||||
- **Default:** `false`
|
||||
- **Example:** `"sandbox": "docker"`
|
||||
|
||||
- **`toolDiscoveryCommand`** (string):
|
||||
|
||||
- **Description:** Defines a custom shell command for discovering tools from your project. The shell command must return on `stdout` a JSON array of [function declarations](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations). Tool wrappers are optional.
|
||||
- **Default:** Empty
|
||||
- **Example:** `"toolDiscoveryCommand": "bin/get_tools"`
|
||||
|
||||
- **`toolCallCommand`** (string):
|
||||
|
||||
- **Description:** Defines a custom shell command for calling a specific tool that was discovered using `toolDiscoveryCommand`. The shell command must meet the following criteria:
|
||||
- It must take function `name` (exactly as in [function declaration](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations)) as first command line argument.
|
||||
- It must read function arguments as JSON on `stdin`, analogous to [`functionCall.args`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functioncall).
|
||||
- It must return function output as JSON on `stdout`, analogous to [`functionResponse.response.content`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functionresponse).
|
||||
- **Default:** Empty
|
||||
- **Example:** `"toolCallCommand": "bin/call_tool"`
|
||||
|
||||
- **`mcpServers`** (object):
|
||||
|
||||
- **Description:** Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Gemini CLI attempts to connect to each configured MCP server to discover available tools. If multiple MCP servers expose a tool with the same name, the tool names will be prefixed with the server alias you defined in the configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note that the system might strip certain schema properties from MCP tool definitions for compatibility.
|
||||
- **Default:** Empty
|
||||
- **Properties:**
|
||||
- **`<SERVER_NAME>`** (object): The server parameters for the named server.
|
||||
- `command` (string, required): The command to execute to start the MCP server.
|
||||
- `args` (array of strings, optional): Arguments to pass to the command.
|
||||
- `env` (object, optional): Environment variables to set for the server process.
|
||||
- `cwd` (string, optional): The working directory in which to start the server.
|
||||
- `timeout` (number, optional): Timeout in milliseconds for requests to this MCP server.
|
||||
- `trust` (boolean, optional): Trust this server and bypass all tool call confirmations.
|
||||
- **Example:**
|
||||
```json
|
||||
"mcpServers": {
|
||||
"myPythonServer": {
|
||||
"command": "python",
|
||||
"args": ["mcp_server.py", "--port", "8080"],
|
||||
"cwd": "./mcp_tools/python",
|
||||
"timeout": 5000
|
||||
},
|
||||
"myNodeServer": {
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js"],
|
||||
"cwd": "./mcp_tools/node"
|
||||
},
|
||||
"myDockerServer": {
|
||||
"command": "docker",
|
||||
"args": ["run", "i", "--rm", "-e", "API_KEY", "ghcr.io/foo/bar"],
|
||||
"env": {
|
||||
"API_KEY": "$MY_API_TOKEN"
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- **`mcpServerCommand`** (string, **deprecated**):
|
||||
|
||||
- This is a legacy setting for configuring a single MCP server. Use `mcpServers` instead.
|
||||
|
||||
### Example `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "GitHub",
|
||||
"sandbox": "docker",
|
||||
"toolDiscoveryCommand": "bin/get_tools",
|
||||
"toolCallCommand": "bin/call_tool",
|
||||
"mcpServers": {
|
||||
"mainServer": {
|
||||
"command": "bin/mcp_server.py"
|
||||
},
|
||||
"anotherServer": {
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js", "--verbose"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables & `.env` Files
|
||||
|
||||
Environment variables are a common way to configure applications, especially for sensitive information like API keys or for settings that might change between environments.
|
||||
|
||||
The CLI automatically loads environment variables from an `.env` file. The loading order is:
|
||||
|
||||
1. `.env` file in the current working directory.
|
||||
2. If not found, it searches upwards in parent directories until it finds an `.env` file or reaches the project root (identified by a `.git` folder) or the home directory.
|
||||
3. If still not found, it looks for `~/.env` (in the user's home directory).
|
||||
|
||||
- **`GEMINI_API_KEY`** (Required):
|
||||
- Your API key for the Gemini API.
|
||||
- **Crucial for operation.** The CLI will not function without it.
|
||||
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env` file.
|
||||
- **`GEMINI_MODEL`**:
|
||||
- Specifies the default Gemini model to use.
|
||||
- Overrides the hardcoded default, which is currently `gemini-2.5-pro-preview-05-06`.
|
||||
- Example: `export GEMINI_MODEL="gemini-1.5-flash-latest"`
|
||||
- **`GOOGLE_API_KEY`**:
|
||||
- Your Google Cloud API key.
|
||||
- Required for using Vertex AI in express mode.
|
||||
- Ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
|
||||
- Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_ KEY"`.
|
||||
- **`GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID.
|
||||
- Required for using Code Assist or Vertex AI.
|
||||
- If using Vertex AI, ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
|
||||
- Example: `export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID for Telemetry in Google Cloud
|
||||
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
- **`GOOGLE_CLOUD_LOCATION`**:
|
||||
- Your Google Cloud Project Location (e.g., us-central1).
|
||||
- Required for using Vertex AI in non express mode.
|
||||
- If using Vertex AI, ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
|
||||
- Example: `export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"`.
|
||||
- **`GEMINI_CODE_ASSIST`**:
|
||||
- Enables Code Assist functionality.
|
||||
- Accepts `true`, `false`, or a custom command string.
|
||||
- If you are using an Enterprise account you should also set the `GOOGLE_CLOUD_PROJECT` environment variable.
|
||||
- Example: `export GEMINI_CODE_ASSIST=true`.
|
||||
- **`GEMINI_SANDBOX`**:
|
||||
- Alternative to the `sandbox` setting in `settings.json`.
|
||||
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
|
||||
- **`SEATBELT_PROFILE`** (macOS specific):
|
||||
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
|
||||
- `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations.
|
||||
- `strict`: Uses a strict profile that declines operations by default.
|
||||
- `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.gemini/` directory (e.g., `my-project/.gemini/sandbox-macos-custom.sb`).
|
||||
- **`DEBUG` or `DEBUG_MODE`** (often used by underlying libraries or the CLI itself):
|
||||
- Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting.
|
||||
- **`NO_COLOR`**:
|
||||
- Set to any value to disable all color output in the CLI.
|
||||
- **`CLI_TITLE`**:
|
||||
- Set to a string to customize the title of the CLI.
|
||||
- **`CODE_ASSIST_ENDPOINT`**:
|
||||
- Specifies the endpoint for the code assist server.
|
||||
- This is useful for development and testing.
|
||||
|
||||
## Command-Line Arguments
|
||||
|
||||
Arguments passed directly when running the CLI can override other configurations for that specific session.
|
||||
|
||||
- **`--model <model_name>`** (or **`-m <model_name>`**):
|
||||
- Specifies the Gemini model to use for this session.
|
||||
- Example: `npm start -- --model gemini-1.5-pro-latest`
|
||||
- **`--sandbox`** (or **`-s`**):
|
||||
- Enables sandbox mode for this session. The exact behavior might depend on other sandbox configurations (environment variables, settings files).
|
||||
- **`--debug_mode`** (or **`-d`**):
|
||||
- Enables debug mode for this session, providing more verbose output.
|
||||
- **`--question <your_question>`** (or **`-q <your_question>`**):
|
||||
- Used to pass a question directly to the command, especially when piping input to the CLI.
|
||||
- **`--all_files`** (or **`-a`**):
|
||||
- If set, recursively includes all files within the current directory as context for the prompt.
|
||||
- **`--help`** (or **`-h`**):
|
||||
- Displays help information about command-line arguments.
|
||||
|
||||
## Context Files (Hierarchical Instructional Context)
|
||||
|
||||
While not strictly configuration for the CLI's _behavior_, context files (defaulting to `GEMINI.md` but configurable via the `contextFileName` setting) are crucial for configuring the _instructional context_ (also referred to as "memory") provided to the Gemini model. This powerful feature allows you to give project-specific instructions, coding style guides, or any relevant background information to the AI, making its responses more tailored and accurate to your needs. The CLI includes UI elements, such as an indicator in the footer showing the number of loaded context files, to keep you informed about the active context.
|
||||
|
||||
- **Purpose:** These Markdown files contain instructions, guidelines, or context that you want the Gemini model to be aware of during your interactions. The system is designed to manage this instructional context hierarchically.
|
||||
|
||||
### Example Context File Content (e.g., `GEMINI.md`)
|
||||
|
||||
Here's a conceptual example of what a context file at the root of a TypeScript project might contain:
|
||||
|
||||
```markdown
|
||||
# Project: My Awesome TypeScript Library
|
||||
|
||||
## General Instructions:
|
||||
|
||||
- When generating new TypeScript code, please follow the existing coding style.
|
||||
- Ensure all new functions and classes have JSDoc comments.
|
||||
- Prefer functional programming paradigms where appropriate.
|
||||
- All code should be compatible with TypeScript 5.0 and Node.js 18+.
|
||||
|
||||
## Coding Style:
|
||||
|
||||
- Use 2 spaces for indentation.
|
||||
- Interface names should be prefixed with `I` (e.g., `IUserService`).
|
||||
- Private class members should be prefixed with an underscore (`_`).
|
||||
- Always use strict equality (`===` and `!==`).
|
||||
|
||||
## Specific Component: `src/api/client.ts`
|
||||
|
||||
- This file handles all outbound API requests.
|
||||
- When adding new API call functions, ensure they include robust error handling and logging.
|
||||
- Use the existing `fetchWithRetry` utility for all GET requests.
|
||||
|
||||
## Regarding Dependencies:
|
||||
|
||||
- Avoid introducing new external dependencies unless absolutely necessary.
|
||||
- If a new dependency is required, please state the reason.
|
||||
```
|
||||
|
||||
This example demonstrates how you can provide general project context, specific coding conventions, and even notes about particular files or components. The more relevant and precise your context files are, the better the AI can assist you. Project-specific context files are highly encouraged to establish conventions and context.
|
||||
|
||||
- **Hierarchical Loading and Precedence:** The CLI implements a sophisticated hierarchical memory system by loading context files (e.g., `GEMINI.md`) from several locations. Content from files lower in this list (more specific) typically overrides or supplements content from files higher up (more general). The exact concatenation order and final context can be inspected using the `/memory show` command. The typical loading order is:
|
||||
1. **Global Context File:**
|
||||
- Location: `~/.gemini/<contextFileName>` (e.g., `~/.gemini/GEMINI.md` in your user home directory).
|
||||
- Scope: Provides default instructions for all your projects.
|
||||
2. **Project Root & Ancestors Context Files:**
|
||||
- Location: The CLI searches for the configured context file in the current working directory and then in each parent directory up to either the project root (identified by a `.git` folder) or your home directory.
|
||||
- Scope: Provides context relevant to the entire project or a significant portion of it.
|
||||
3. **Sub-directory Context Files (Contextual/Local):**
|
||||
- Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.).
|
||||
- Scope: Allows for highly specific instructions relevant to a particular component, module, or sub-section of your project.
|
||||
- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt to the Gemini model. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
|
||||
- **Commands for Memory Management:**
|
||||
- Use `/memory refresh` to force a re-scan and reload of all context files from all configured locations. This updates the AI's instructional context.
|
||||
- Use `/memory show` to display the combined instructional context currently loaded, allowing you to verify the hierarchy and content being used by the AI.
|
||||
- See the [Commands documentation](./commands.md#memory) for full details on the `/memory` command and its sub-commands (`show` and `refresh`).
|
||||
|
||||
By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor the Gemini CLI's responses to your specific needs and projects.
|
||||
|
||||
## Sandboxing
|
||||
|
||||
The Gemini CLI can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system.
|
||||
|
||||
Sandboxing is disabled by default, but you can enable it in a few ways:
|
||||
|
||||
- Using `--sandbox` or `-s` flag.
|
||||
- Setting `GEMINI_SANDBOX` environment variable.
|
||||
- Sandbox is enabled in `--yolo` mode by default.
|
||||
|
||||
By default, it uses a pre-built `gemini-cli-sandbox` Docker image.
|
||||
|
||||
For project-specific sandboxing needs, you can create a custom Dockerfile at `.gemini/sandbox.Dockerfile` in your project's root directory. This Dockerfile can be based on the base sandbox image:
|
||||
|
||||
```dockerfile
|
||||
FROM gemini-cli-sandbox
|
||||
|
||||
# Add your custom dependencies or configurations here
|
||||
# For example:
|
||||
# RUN apt-get update && apt-get install -y some-package
|
||||
# COPY ./my-config /app/my-config
|
||||
```
|
||||
|
||||
When `.gemini/sandbox.Dockerfile` exists, you can use `BUILD_SANDBOX` environment variable when running Gemini CLI to automatically build the custom sandbox image:
|
||||
|
||||
```bash
|
||||
BUILD_SANDBOX=1 gemini -s
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Gemini CLI
|
||||
|
||||
Within Gemini CLI, `packages/cli` is the frontend for users to send and receive prompts with the Gemini AI model and its associated tools. For a general overview of Gemini CLI, see the [main documentation page](../index.md).
|
||||
|
||||
## Navigating this section
|
||||
|
||||
- **[Commands](./commands.md):** A reference for Gemini CLI commands (e.g., `/help`, `/tools`, `/theme`).
|
||||
- **[Configuration](./configuration.md):** A guide to tailoring Gemini CLI behavior using configuration files.
|
||||
- **[Themes](./themes.md)**: A guide to customizing the CLI's appearance with different themes.
|
||||
- **[Tutorials](tutorials.md)**: A tutorial showing how to use Gemini CLI to automate a development task.
|
||||
|
||||
## Non-interactive mode
|
||||
|
||||
Gemini CLI can be run in a non-interactive mode, which is useful for scripting and automation. In this mode, you pipe input to the CLI, it executes the command, and then it exits.
|
||||
|
||||
The following example pipes a command to Gemini CLI from your terminal:
|
||||
|
||||
```bash
|
||||
echo "What is fine tuning?" | gemini
|
||||
```
|
||||
|
||||
Gemini CLI executes the command and prints the output to your terminal. Note that you can achieve the same behavior by using the `--prompt` or `-p` flag. For example:
|
||||
|
||||
```bash
|
||||
gemini -p "What is fine tuning?"
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
# Themes
|
||||
|
||||
Gemini CLI supports a variety of themes to customize its color scheme and appearance. You can change the theme to suit your preferences via the `/theme` command or `"theme":` configuration setting.
|
||||
|
||||
## Available Themes
|
||||
|
||||
Gemini CLI comes with a selection of pre-defined themes, which you can list using the `/theme` command within Gemini CLI:
|
||||
|
||||
- **Dark Themes:**
|
||||
- `ANSI`
|
||||
- `Atom One`
|
||||
- `Ayu`
|
||||
- `Default`
|
||||
- `Dracula`
|
||||
- `GitHub`
|
||||
- **Light Themes:**
|
||||
- `ANSI Light`
|
||||
- `Ayu Light`
|
||||
- `Default Light`
|
||||
- `GitHub Light`
|
||||
- `Google Code`
|
||||
- `Xcode`
|
||||
|
||||
### Changing Themes
|
||||
|
||||
1. Enter `/theme` into Gemini CLI.
|
||||
2. A dialog or selection prompt appears, listing the available themes.
|
||||
3. Using the arrow keys, select a theme. Some interfaces might offer a live preview or highlight as you select.
|
||||
4. Confirm your selection to apply the theme.
|
||||
|
||||
### Theme Persistence
|
||||
|
||||
Selected themes are saved in Gemini CLI's [configuration](./docs/cli/configuration.md) so your preference is remembered across sessions.
|
||||
|
||||
## Dark Themes
|
||||
|
||||
### ANSI
|
||||
|
||||
<img src="../assets/theme-ansi.png" alt="ANSI theme" width="600" />
|
||||
|
||||
### Atom OneDark
|
||||
|
||||
<img src="../assets/theme-atom-one.png" alt="Atom One theme" width="600">
|
||||
|
||||
### Ayu
|
||||
|
||||
<img src="../assets/theme-ayu.png" alt="Ayu theme" width="600">
|
||||
|
||||
### Default
|
||||
|
||||
<img src="../assets/theme-default.png" alt="Default theme" width="600">
|
||||
|
||||
### Dracula
|
||||
|
||||
<img src="../assets/theme-dracula.png" alt="Dracula theme" width="600">
|
||||
|
||||
### GitHub
|
||||
|
||||
<img src="../assets/theme-github.png" alt="GitHub theme" width="600">
|
||||
|
||||
## Light Themes
|
||||
|
||||
### ANSI Light
|
||||
|
||||
<img src="../assets/theme-ansi-light.png" alt="ANSI Light theme" width="600">
|
||||
|
||||
### Ayu Light
|
||||
|
||||
<img src="../assets/theme-ayu-light.png" alt="Ayu Light theme" width="600">
|
||||
|
||||
### Default Light
|
||||
|
||||
<img src="../assets/theme-default-light.png" alt="Default Light theme" width="600">
|
||||
|
||||
### GitHub Light
|
||||
|
||||
<img src="../assets/theme-github-light.png" alt="GitHub Light theme" width="600">
|
||||
|
||||
### Google Code
|
||||
|
||||
<img src="../assets/theme-google-light.png" alt="Google Code theme" width="600">
|
||||
|
||||
### Xcode
|
||||
|
||||
<img src="../assets/theme-xcode-light.png" alt="Xcode Light theme" width="600">
|
||||
@@ -0,0 +1,69 @@
|
||||
# Tutorials
|
||||
|
||||
This page contains tutorials for interacting with Gemini CLI.
|
||||
|
||||
## Setting up a Model Context Protocol (MCP) server
|
||||
|
||||
> [!CAUTION]
|
||||
> Before using a third-party MCP server, ensure you trust its source and understand the tools it provides. Your use of third-party servers is at your own risk.
|
||||
|
||||
This tutorial demonstrates how to set up a MCP server, using the [GitHub MCP server](https://github.com/github/github-mcp-server) as an example. The GitHub MCP server provides tools for interacting with GitHub repositories, such as creating issues and commenting on pull requests.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following installed and configured:
|
||||
|
||||
- **Docker:** Install and run [Docker].
|
||||
- **GitHub Personal Access Token (PAT):** Create a new [classic] or [fine-grained] PAT with the necessary scopes.
|
||||
|
||||
[Docker]: https://www.docker.com/
|
||||
[classic]: https://github.com/settings/tokens/new
|
||||
[fine-grained]: https://github.com/settings/personal-access-tokens/new
|
||||
|
||||
### Guide
|
||||
|
||||
#### Configure the MCP server in `settings.json`
|
||||
|
||||
In your project's root directory, create or open the [`.gemini/settings.json` file](./configuration.md). Within the file, add the `mcpServers` configuration block, which provides instructions for how to launch the GitHub MCP server.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Set your GitHub token
|
||||
|
||||
> [!CAUTION]
|
||||
> Using a broadly scoped personal access token that has access to personal and private repositories can lead to information from the private repository being leaked into the public repository. We recommend using a fine-grained access token that doesn't share access to both public and private repositories.
|
||||
|
||||
Use an environment variable to store your GitHub PAT:
|
||||
|
||||
```bash
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN="pat_YourActualGitHubTokenHere"
|
||||
```
|
||||
|
||||
Gemini CLI uses this value in the `mcpServers` configuration that you defined in the `settings.json` file.
|
||||
|
||||
#### Launch Gemini CLI and verify the connection
|
||||
|
||||
When you launch Gemini CLI, it automatically reads your configuration and launches the GitHub MCP server in the background. You can then use natural language prompts to ask Gemini CLI to perform GitHub actions. For example:
|
||||
|
||||
```bash
|
||||
"get all open issues assigned to me in the 'foo/bar' repo and prioritize them"
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# Gemini CLI Core
|
||||
|
||||
The Gemini CLI Core (`packages/core`) acts as the backend engine for the Gemini CLI, handling communication with the Gemini API, managing tools, and processing requests from the CLI client. For a general overview of the Gemini CLI, see the [main documentation page](../index.md).
|
||||
|
||||
## Navigating this Section
|
||||
|
||||
- **[Core Configuration](../cli/configuration.md):** Details on how to configure the core component, including environment variables and specific settings.
|
||||
- **[Core Tools API](./tools-api.md):** Information on how tools are defined, registered, and used by the core.
|
||||
|
||||
## Role of the Core
|
||||
|
||||
The core package is a crucial part of the Gemini CLI ecosystem. While the CLI (`packages/cli`) provides the user interface, the core is responsible for:
|
||||
|
||||
- **API Interaction:** Securely communicating with the Google Gemini API, sending user prompts, and receiving model responses.
|
||||
- **Prompt Engineering:** Constructing effective prompts for the Gemini model, potentially incorporating conversation history, tool definitions, and instructional context from `GEMINI.md` files.
|
||||
- **Tool Management & Orchestration:**
|
||||
- Registering available tools (e.g., file system tools, shell command execution).
|
||||
- Interpreting tool use requests from the Gemini model.
|
||||
- Executing the requested tools with the provided arguments.
|
||||
- Returning tool execution results to the Gemini model for further processing.
|
||||
- **Session and State Management:** Keeping track of the conversation state, including history and any relevant context required for coherent interactions.
|
||||
- **Configuration:** Managing core-specific configurations, such as API key access, model selection, and tool settings.
|
||||
|
||||
## Key Components and Functionality
|
||||
|
||||
While the exact implementation details are within the `packages/core/src/` directory, key conceptual components include:
|
||||
|
||||
- **API Client** (`client.ts`): A module responsible for making HTTP requests to the Gemini API, handling authentication, and parsing responses.
|
||||
- **Prompt Management** (`prompts.ts`): Logic for creating and formatting the prompts sent to the Gemini model. This includes integrating user queries, historical context, and tool specifications.
|
||||
- **Tool Registry and Execution** (`tool-registry.ts`, `tools.ts`, individual tool files like `read-file.ts`, `shell.ts`):
|
||||
- A system for discovering, registering, and describing available tools to the Gemini model.
|
||||
- Code for executing each tool safely and effectively, often involving interaction with the operating system or external services.
|
||||
- **Configuration (`config.ts`):** Handles loading and providing access to core-side configurations, including API keys, model choices, and potentially tool-specific settings.
|
||||
- **Turn Management (`turn.ts`):** Manages the flow of a single conversational turn, from receiving user input to generating a final response, potentially involving multiple tool calls.
|
||||
|
||||
## Interaction with the CLI
|
||||
|
||||
The CLI and Core typically communicate over a local interface (e.g., standard input/output, or a local network connection if designed for broader use, though the current structure suggests a tightly coupled Node.js application).
|
||||
|
||||
1. The CLI captures user input and forwards it to the Core.
|
||||
2. The Core processes the input, interacts with the Gemini API and tools as needed.
|
||||
3. The Core sends responses (text, tool calls, errors) back to the CLI.
|
||||
4. The CLI formats and displays these responses to the user.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
The core plays a vital role in security:
|
||||
|
||||
- **API Key Management:** It handles the `GEMINI_API_KEY` and ensures it is used securely when communicating with the Gemini API.
|
||||
- **Tool Execution:** When tools interact with the local system (e.g., `run_shell_command`), the core (and its underlying tool implementations) must do so with appropriate caution, often involving sandboxing mechanisms to prevent unintended side effects.
|
||||
|
||||
## Chat History Compression
|
||||
|
||||
To ensure that long conversations don't exceed the token limits of the Gemini model, the CLI includes a chat history compression feature.
|
||||
|
||||
When a conversation approaches the token limit for the configured model, the CLI will automatically compress the conversation history before sending it to the model. This compression is designed to be lossless in terms of the information conveyed, but it reduces the overall number of tokens used.
|
||||
|
||||
You can find the token limits for each model in the [Google AI documentation](https://ai.google.dev/gemini-api/docs/models).
|
||||
|
||||
## Model Fallback
|
||||
|
||||
The Gemini CLI includes a model fallback mechanism to ensure that you can continue to use the CLI even if the default "pro" model is rate-limited.
|
||||
|
||||
If you are using the default "pro" model and the CLI detects that you are being rate-limited, it will automatically switch to the "flash" model for the current session. This allows you to continue working without interruption.
|
||||
|
||||
## File Discovery Service
|
||||
|
||||
The file discovery service is responsible for finding files in the project that are relevant to the current context. It is used by the `@` command and other tools that need to access files.
|
||||
|
||||
## Memory Discovery Service
|
||||
|
||||
The memory discovery service is responsible for finding and loading the `GEMINI.md` files that provide context to the model. It searches for these files in a hierarchical manner, starting from the current working directory and moving up to the project root and the user's home directory. It also searches in subdirectories.
|
||||
|
||||
This allows you to have global, project-level, and component-level context files, which are all combined to provide the model with the most relevant information.
|
||||
|
||||
You can use the `/memory show` command to see the combined content of all loaded `GEMINI.md` files, and the `/memory refresh` command to reload them.
|
||||
@@ -0,0 +1,287 @@
|
||||
# Gemini CLI Observability Guide
|
||||
|
||||
Telemetry provides crucial data about the Gemini CLI's performance, health, and usage. By enabling it, you can monitor operations, debug issues, and optimize tool usage through traces, metrics, and structured logs.
|
||||
|
||||
This entire system is built on the **[OpenTelemetry] (OTEL)** standard, allowing you to send data to any compatible backend, from your local terminal to a cloud service.
|
||||
|
||||
[OpenTelemetry]: https://opentelemetry.io/
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Telemetry with Google Cloud
|
||||
|
||||
1. **Ensure Prerequisites:**
|
||||
Ensure that:
|
||||
- You have exported the `OTLP_GOOGLE_CLOUD_PROJECT` environment variable.
|
||||
- You have authenticated with Google Cloud and have the necessary IAM roles.
|
||||
For full details, see the [Google Cloud](#google-cloud) prerequisites.
|
||||
1. **Run the Command:** Execute the following command from the project root:
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
1. **Run Gemini CLI:** In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
|
||||
1. **View Data:** The script will provide links to view your telemetry data (traces, metrics, logs) in the Google Cloud Console.
|
||||
1. **Details:** Refer to documentation for telemetry in [Google Cloud](#google-cloud).
|
||||
|
||||
### Local Telemetry with Jaeger UI (for Traces)
|
||||
|
||||
1. **Run the Command:** Execute the following command from the project root:
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
2. **Run Gemini CLI:** In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
|
||||
3. **View Logs/Metrics:** Check the `.gemini/otel/collector.log` file for raw logs and metrics.
|
||||
4. **View Traces:** Open your browser and go to `http://localhost:16686` to see traces in the Jaeger UI.
|
||||
5. **Details:** Refer to documentation for telemetry in [Local](#local).
|
||||
|
||||
## Enabling Telemetry
|
||||
|
||||
You can enable telemetry in multiple ways. [Configuration](configuration.md) is primarily managed via the `.gemini/settings.json` file and environment variables, but CLI flags can override these settings for a specific session.
|
||||
|
||||
> **A Note on Sandbox Mode:** Telemetry is not compatible with sandbox mode at this time. Turn off sandbox mode before enabling telemetry. Tracked in #894.
|
||||
|
||||
**Order of Precedence:**
|
||||
|
||||
Telemetry settings are resolved in the following order (highest precedence first):
|
||||
|
||||
1. **CLI Flags (for `gemini` command):**
|
||||
- `--telemetry` / `--no-telemetry`: Overrides `telemetry.enabled`. If this flag is not provided, telemetry is disabled unless enabled in settings files.
|
||||
- `--telemetry-target <local|gcp>`: Overrides `telemetry.target`.
|
||||
- `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`.
|
||||
- `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`.
|
||||
2. **Environment Variables:**
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint` if no `--telemetry-otlp-endpoint` flag is present.
|
||||
3. **Workspace Settings File (`.gemini/settings.json`):** Values from the `telemetry` object in this project-specific file.
|
||||
4. **User Settings File (`~/.gemini/settings.json`):** Values from the `telemetry` object in this global user file.
|
||||
5. **Defaults:** applied if not set by any of the above.
|
||||
- `telemetry.enabled`: `false`
|
||||
- `telemetry.target`: `local`
|
||||
- `telemetry.otlpEndpoint`: `http://localhost:4317`
|
||||
- `telemetry.logPrompts`: `true`
|
||||
|
||||
**For the `npm run telemetry -- --target=<gcp|local>` script:**
|
||||
The `--target` argument to this script _only_ overrides the `telemetry.target` for the duration and purpose of that script (i.e., choosing which collector to start). It does not permanently change your `settings.json`. The script will first look at `settings.json` for a `telemetry.target` to use as its default.
|
||||
|
||||
**Example settings:**
|
||||
Add these lines to configure telemetry in your workspace (`.gemini/settings.json`) or user (`~/.gemini/settings.json`) settings for GCP:
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
},
|
||||
"sandbox": false
|
||||
}
|
||||
```
|
||||
|
||||
## Running an OTEL Collector
|
||||
|
||||
An OTEL Collector is a service that receives, processes, and exports telemetry data.
|
||||
The CLI sends data using the OTLP/gRPC protocol.
|
||||
|
||||
Learn more about OTEL exporter standard configuration in [documentation][otel-config-docs].
|
||||
|
||||
[otel-config-docs]: https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
|
||||
|
||||
### Configuration
|
||||
|
||||
1. Install [otelcol-contrib] or use [docker]
|
||||
|
||||
[otelcol-contrib]: https://github.com/open-telemetry/opentelemetry-collector-contrib
|
||||
[docker]: https://www.docker.com/
|
||||
|
||||
2. Create a folder for the OTEL configurations:
|
||||
|
||||
```
|
||||
mkdir .gemini/otel
|
||||
```
|
||||
|
||||
### Local
|
||||
|
||||
Use the `npm run telemetry -- --target=local` command which automates the entire process of setting up a local telemetry pipeline, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib` (The OpenTelemetry Collector) and `jaeger` (The Jaeger UI for viewing traces). To use it:
|
||||
|
||||
1. **Run the Command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
- Download Jaeger and OTEL if needed.
|
||||
- Start a local Jaeger instance.
|
||||
- Start an OTEL collector configured to receive data from the Gemini CLI.
|
||||
- Automatically enable telemetry in your workspace settings.
|
||||
- On exit, disable telemetry.
|
||||
|
||||
2. **View Traces**:
|
||||
Open your web browser and navigate to **http://localhost:16686** to access the Jaeger UI. Here you can inspect detailed traces of Gemini CLI operations.
|
||||
|
||||
3. **Inspect Logs and Metrics**:
|
||||
The script redirects the OTEL collector's output (which includes logs and metrics) to `.gemini/otel/collector.log`. You can monitor this file to see the raw telemetry data:
|
||||
```bash
|
||||
tail -f .gemini/otel/collector.log
|
||||
```
|
||||
4. **Stop the Services**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector and Jaeger services.
|
||||
|
||||
### Google Cloud
|
||||
|
||||
Use the `npm run telemetry -- --target=gcp` command which automates setting up a local OpenTelemetry collector that forwards data to your Google Cloud project, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib`. To use it:
|
||||
|
||||
1. **Prerequisites**:
|
||||
|
||||
- Ensure you have a Google Cloud Project ID.
|
||||
- Export the `GOOGLE_CLOUD_PROJECT` environment variable to make it available to the OTEL collector.
|
||||
```bash
|
||||
export OTLP_GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
```
|
||||
- Authenticate with Google Cloud (e.g., run `gcloud auth application-default login` or ensure `GOOGLE_APPLICATION_CREDENTIALS` is set).
|
||||
- Ensure your account/service account has the necessary roles: "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer".
|
||||
|
||||
2. **Run the Command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
- Download the `otelcol-contrib` binary if needed.
|
||||
- Start an OTEL collector configured to receive data from the Gemini CLI and export it to your specified Google Cloud project.
|
||||
- Automatically enable telemetry and disable sandbox mode in your workspace settings (`.gemini/settings.json`).
|
||||
- Provide direct links to view traces, metrics, and logs in your Google Cloud Console.
|
||||
- On exit (Ctrl+C), it will attempt to restore your original telemetry and sandbox settings.
|
||||
|
||||
3. **Run Gemini CLI:**
|
||||
In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
|
||||
|
||||
4. **View Telemetry in Google Cloud**:
|
||||
Use the links provided by the script to navigate to the Google Cloud Console and view your traces, metrics, and logs.
|
||||
|
||||
5. **Inspect Local Collector Logs**:
|
||||
The script redirects the local OTEL collector\'s output to `.gemini/otel/collector-gcp.log`. You can monitor this file for detailed information or troubleshooting:
|
||||
|
||||
```bash
|
||||
tail -f .gemini/otel/collector-gcp.log
|
||||
```
|
||||
|
||||
6. **Stop the Service**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector.
|
||||
|
||||
## Data Reference: Logs & Metrics
|
||||
|
||||
A `sessionId` is included as a common attribute on all logs and metrics.
|
||||
|
||||
### Logs
|
||||
|
||||
These are timestamped records of specific events.
|
||||
|
||||
- `gemini_cli.config`: Fired once at startup with the CLI's configuration.
|
||||
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `embedding_model` (string)
|
||||
- `sandbox_enabled` (boolean)
|
||||
- `core_tools_enabled` (string)
|
||||
- `approval_mode` (string)
|
||||
- `api_key_enabled` (boolean)
|
||||
- `vertex_ai_enabled` (boolean)
|
||||
- `code_assist_enabled` (boolean)
|
||||
- `log_prompts_enabled` (boolean)
|
||||
- `file_filtering_respect_git_ignore` (boolean)
|
||||
- `debug_mode` (boolean)
|
||||
- `mcp_servers` (string)
|
||||
|
||||
- `gemini_cli.user_prompt`: Fired when a user submits a prompt.
|
||||
|
||||
- **Attributes**:
|
||||
- `prompt_length`
|
||||
- `prompt` (except if `log_prompts_enabled` is false)
|
||||
|
||||
- `gemini_cli.tool_call`: Fired for every function call.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `function_args`
|
||||
- `duration_ms`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", or "modify", optional)
|
||||
- `error` (optional)
|
||||
- `error_type` (optional)
|
||||
|
||||
- `gemini_cli.api_request`: Fired when making a request to the Gemini API.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `request_text` (optional)
|
||||
|
||||
- `gemini_cli.api_error`: Fired if the API request fails.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `error`
|
||||
- `error_type`
|
||||
- `status_code`
|
||||
- `duration_ms`
|
||||
|
||||
- `gemini_cli.api_response`: Fired upon receiving a response from the Gemini API.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `status_code`
|
||||
- `duration_ms`
|
||||
- `error` (optional)
|
||||
- `input_token_count`
|
||||
- `output_token_count`
|
||||
- `cached_content_token_count`
|
||||
- `thoughts_token_count`
|
||||
- `tool_token_count`
|
||||
- `response_text` (optional)
|
||||
|
||||
### Metrics
|
||||
|
||||
These are numerical measurements of behavior over time.
|
||||
|
||||
- `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup.
|
||||
|
||||
- `gemini_cli.tool.call.count` (Counter, Int): Counts tool calls.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", or "modify", optional)
|
||||
|
||||
- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `decision` (string: "accept", "reject", or "modify", optional)
|
||||
|
||||
- `gemini_cli.api.request.count` (Counter, Int): Counts all API requests.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `status_code`
|
||||
- `error_type` (optional)
|
||||
|
||||
- `gemini_cli.api.request.latency` (Histogram, ms): Measures API request latency.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
|
||||
- `gemini_cli.token.usage` (Counter, Int): Counts the number of tokens used.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `type` (string: "input", "output", "thought", "cache", or "tool")
|
||||
|
||||
- `gemini_cli.file.operation.count` (Counter, Int): Counts file operations.
|
||||
|
||||
- **Attributes**:
|
||||
- `operation` (string: "create", "read", "update"): The type of file operation.
|
||||
- `lines` (optional, Int): Number of lines in the file.
|
||||
- `mimetype` (optional, string): Mimetype of the file.
|
||||
- `extension` (optional, string): File extension of the file.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Gemini CLI Core: Tools API
|
||||
|
||||
The Gemini CLI core (`packages/core`) features a robust system for defining, registering, and executing tools. These tools extend the capabilities of the Gemini model, allowing it to interact with the local environment, fetch web content, and perform various actions beyond simple text generation.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Tool (`tools.ts`):** An interface and base class (`BaseTool`) that defines the contract for all tools. Each tool must have:
|
||||
|
||||
- `name`: A unique internal name (used in API calls to Gemini).
|
||||
- `displayName`: A user-friendly name.
|
||||
- `description`: A clear explanation of what the tool does, which is provided to the Gemini model.
|
||||
- `parameterSchema`: A JSON schema defining the parameters the tool accepts. This is crucial for the Gemini model to understand how to call the tool correctly.
|
||||
- `validateToolParams()`: A method to validate incoming parameters.
|
||||
- `getDescription()`: A method to provide a human-readable description of what the tool will do with specific parameters before execution.
|
||||
- `shouldConfirmExecute()`: A method to determine if user confirmation is required before execution (e.g., for potentially destructive operations).
|
||||
- `execute()`: The core method that performs the tool's action and returns a `ToolResult`.
|
||||
|
||||
- **`ToolResult` (`tools.ts`):** An interface defining the structure of a tool's execution outcome:
|
||||
|
||||
- `llmContent`: The factual string content to be included in the history sent back to the LLM for context.
|
||||
- `returnDisplay`: A user-friendly string (often Markdown) or a special object (like `FileDiff`) for display in the CLI.
|
||||
|
||||
- **Tool Registry (`tool-registry.ts`):** A class (`ToolRegistry`) responsible for:
|
||||
- **Registering Tools:** Holding a collection of all available built-in tools (e.g., `ReadFileTool`, `ShellTool`).
|
||||
- **Discovering Tools:** It can also discover tools dynamically:
|
||||
- **Command-based Discovery:** If `toolDiscoveryCommand` is configured in settings, this command is executed. It's expected to output JSON describing custom tools, which are then registered as `DiscoveredTool` instances.
|
||||
- **MCP-based Discovery:** If `mcpServerCommand` is configured, the registry can connect to a Model Context Protocol (MCP) server to list and register tools (`DiscoveredMCPTool`).
|
||||
- **Providing Schemas:** Exposing the `FunctionDeclaration` schemas of all registered tools to the Gemini model, so it knows what tools are available and how to use them.
|
||||
- **Retrieving Tools:** Allowing the core to get a specific tool by name for execution.
|
||||
|
||||
## Built-in Tools
|
||||
|
||||
The core comes with a suite of pre-defined tools, typically found in `packages/core/src/tools/`. These include:
|
||||
|
||||
- **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.
|
||||
- `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.
|
||||
- `EditTool` (`edit.ts`): Performs in-place modifications to files (often requiring confirmation).
|
||||
- `ReadManyFilesTool` (`read-many-files.ts`): Reads and concatenates content from multiple files or glob patterns (used by the `@` command in CLI).
|
||||
- **Execution Tools:**
|
||||
- `ShellTool` (`shell.ts`): Executes arbitrary shell commands (requires careful sandboxing and user confirmation).
|
||||
- **Web Tools:**
|
||||
- `WebFetchTool` (`web-fetch.ts`): Fetches content from a URL.
|
||||
|
||||
Each of these tools extends `BaseTool` and implements the required methods for its specific functionality.
|
||||
|
||||
## Tool Execution Flow
|
||||
|
||||
1. **Model Request:** The Gemini model, based on the user's prompt and the provided tool schemas, decides to use a tool and returns a `FunctionCall` part in its response, specifying the tool name and arguments.
|
||||
2. **Core Receives Request:** The core parses this `FunctionCall`.
|
||||
3. **Tool Retrieval:** It looks up the requested tool in the `ToolRegistry`.
|
||||
4. **Parameter Validation:** The tool's `validateToolParams()` method is called.
|
||||
5. **Confirmation (if needed):**
|
||||
- The tool's `shouldConfirmExecute()` method is called.
|
||||
- If it returns details for confirmation, the core communicates this back to the CLI, which prompts the user.
|
||||
- The user's decision (e.g., proceed, cancel) is sent back to the core.
|
||||
6. **Execution:** If validated and confirmed (or if no confirmation is needed), the core calls the tool's `execute()` method with the provided arguments and an `AbortSignal` (for potential cancellation).
|
||||
7. **Result Processing:** The `ToolResult` from `execute()` is received by the core.
|
||||
8. **Response to Model:** The `llmContent` from the `ToolResult` is packaged as a `FunctionResponse` and sent back to the Gemini model so it can continue generating a user-facing response.
|
||||
9. **Display to User:** The `returnDisplay` from the `ToolResult` is sent to the CLI to show the user what the tool did.
|
||||
|
||||
## Extending with Custom Tools
|
||||
|
||||
While direct programmatic registration of new tools by users isn't explicitly detailed as a primary workflow in the provided files for typical end-users, the architecture supports extension through:
|
||||
|
||||
- **Command-based Discovery:** Advanced users or project administrators can define a `toolDiscoveryCommand` in `settings.json`. This command, when run by the Gemini CLI core, should output a JSON array of `FunctionDeclaration` objects. The core will then make these available as `DiscoveredTool` instances. The corresponding `toolCallCommand` would then be responsible for actually executing these custom tools.
|
||||
- **MCP Server(s):** For more complex scenarios, one or more MCP servers can be set up and configured via the `mcpServers` setting in `settings.json`. The Gemini CLI core can then discover and use tools exposed by these servers. As mentioned, if you have multiple MCP servers, the tool names will be prefixed with the server name from your configuration (e.g., `serverAlias__actualToolName`).
|
||||
|
||||
This tool system provides a flexible and powerful way to augment the Gemini model's capabilities, making the Gemini CLI a versatile assistant for a wide range of tasks.
|
||||
@@ -0,0 +1,116 @@
|
||||
# Gemini CLI Execution and Deployment
|
||||
|
||||
This document describes how to run Gemini CLI and explains the deployment architecture that Gemini CLI uses.
|
||||
|
||||
## Running Gemini CLI
|
||||
|
||||
There are several ways to run Gemini CLI. The option you choose depends on how you intend to use Gemini CLI.
|
||||
|
||||
---
|
||||
|
||||
### 1. Standard installation (Recommended for typical users)
|
||||
|
||||
This is the recommended way for end-users to install Gemini CLI. It involves downloading the Gemini CLI package from the NPM registry.
|
||||
|
||||
- **Global install:**
|
||||
|
||||
```bash
|
||||
# Install the CLI globally
|
||||
npm install -g @gemini-cli/cli
|
||||
|
||||
# Now you can run the CLI from anywhere
|
||||
gemini
|
||||
```
|
||||
|
||||
- **NPX execution:**
|
||||
```bash
|
||||
# Execute the latest version from NPM without a global install
|
||||
npx @gemini-cli/cli
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Running in a sandbox (Docker/Podman)
|
||||
|
||||
For security and isolation, Gemini CLI can be run inside a container. This is the default way that the CLI executes tools that might have side effects.
|
||||
|
||||
- **Directly from the Registry:**
|
||||
You can run the published sandbox image directly. This is useful for environments where you only have Docker and want to run the CLI.
|
||||
```bash
|
||||
# Run the published sandbox image
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.0
|
||||
```
|
||||
- **Using the `--sandbox` flag:**
|
||||
If you have Gemini CLI installed locally (using the standard installation described above), you can instruct it to run inside the sandbox container.
|
||||
```bash
|
||||
gemini --sandbox "your prompt here"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Running from source (Recommended for Gemini CLI contributors)
|
||||
|
||||
Contributors to the project will want to run the CLI directly from the source code.
|
||||
|
||||
- **Development Mode:**
|
||||
This method provides hot-reloading and is useful for active development.
|
||||
```bash
|
||||
# From the root of the repository
|
||||
npm run start
|
||||
```
|
||||
- **Production-like mode (Linked package):**
|
||||
This method simulates a global installation by linking your local package. It's useful for testing a local build in a production workflow.
|
||||
|
||||
```bash
|
||||
# Link the local cli package to your global node_modules
|
||||
npm link packages/cli
|
||||
|
||||
# Now you can run your local version using the `gemini` command
|
||||
gemini
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Running the latest Gemini CLI commit from GitHub
|
||||
|
||||
You can run the most recently committed version of Gemini CLI directly from the GitHub repository. This is useful for testing features still in developement.
|
||||
|
||||
```bash
|
||||
# Execute the CLI directly from the main branch on GitHub
|
||||
npx https://github.com/google/gemini-cli
|
||||
```
|
||||
|
||||
## Deployment architecture
|
||||
|
||||
The execution methods described above are made possible by the following architectural components and processes:
|
||||
|
||||
**NPM packages**
|
||||
|
||||
Gemini CLI project is a monorepo that publishes two core packages to the NPM registry:
|
||||
|
||||
- `@gemini-cli/core`: The backend, handling logic and tool execution.
|
||||
- `@gemini-cli/cli`: The user-facing frontend.
|
||||
|
||||
These packages are used when performing the standard installation and when running Gemini CLI from the source.
|
||||
|
||||
**Build and packaging processes**
|
||||
|
||||
There are two distinct build processes used, depending on the distribution channel:
|
||||
|
||||
- **NPM publication:** For publishing to the NPM registry, the TypeScript source code in `@gemini-cli/core` and `@gemini-cli/cli` is transpiled into standard JavaScript using the TypeScript Compiler (`tsc`). The resulting `dist/` directory is what gets published in the NPM package. This is a standard approach for TypeScript libraries.
|
||||
|
||||
- **GitHub `npx` execution:** When running the latest version of Gemini CLI directly from GitHub, a different process is triggered by the `prepare` script in `package.json`. This script uses `esbuild` to bundle the entire application and its dependencies into a single, self-contained JavaScript file. This bundle is created on-the-fly on the user's machine and is not checked into the repository.
|
||||
|
||||
**Docker sandbox image**
|
||||
|
||||
The Docker-based execution method is supported by the `gemini-cli-sandbox` container image. This image is published to a container registry and contains a pre-installed, global version of Gemini CLI. The `scripts/prepare-cli-packagejson.js` script dynamically injects the URI of this image into the CLI's `package.json` before publishing, so the CLI knows which image to pull when the `--sandbox` flag is used.
|
||||
|
||||
## Release process
|
||||
|
||||
A unified script, `npm run publish:release`, orchestrates the release process. The script performs the following actions:
|
||||
|
||||
1. Build the NPM packages using `tsc`.
|
||||
2. Update the CLI's `package.json` with the Docker image URI.
|
||||
3. Build and tag the `gemini-cli-sandbox` Docker image.
|
||||
4. Push the Docker image to the container registry.
|
||||
5. Publish the NPM packages to the artifact registry.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Gemini CLI Extensions
|
||||
|
||||
Gemini CLI supports extensions that can be used to configure and extend its functionality.
|
||||
|
||||
## How it works
|
||||
|
||||
On startup, Gemini CLI looks for extensions in two locations:
|
||||
|
||||
1. `<workspace>/.gemini/extensions`
|
||||
2. `<home>/.gemini/extensions`
|
||||
|
||||
Gemini CLI loads all extensions from both locations. If an extension with the same name exists in both locations, the extension in the workspace directory takes precedence.
|
||||
|
||||
Within each location, individual extensions exist as a directory that contains a `gemini-extension.json` file. For example:
|
||||
|
||||
`<workspace>/.gemini/extensions/my-extension/gemini-extension.json`
|
||||
|
||||
### `gemini-extension.json`
|
||||
|
||||
The `gemini-extension.json` file contains the configuration for the extension. The file has the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-extension",
|
||||
"version": "1.0.0",
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "node my-server.js"
|
||||
}
|
||||
},
|
||||
"contextFileName": "GEMINI.md"
|
||||
}
|
||||
```
|
||||
|
||||
- `name`: The name of the extension. This is used to uniquely identify the extension. This should match the name of your extension directory.
|
||||
- `version`: The version of the extension.
|
||||
- `mcpServers`: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a [`settings.json` file](./cli/configuration.md). If both an extension and a `settings.json` file configure an MCP server with the same name, the server defined in the `settings.json` file takes precedence.
|
||||
- `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the workspace. If this property is not used but a `GEMINI.md` file is present in your extension directory, then that file will be loaded.
|
||||
|
||||
When Gemini CLI starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Welcome to Gemini CLI documentation
|
||||
|
||||
This documentation provides a comprehensive guide to installing, using, and developing Gemini CLI. This tool lets you interact with Gemini models through a command-line interface.
|
||||
|
||||
## Overview
|
||||
|
||||
Gemini CLI brings the capabilities of Gemini models to your terminal in an interactive Read-Eval-Print Loop (REPL) environment. Gemini CLI consists of a client-side application (`packages/cli`) that communicates with a local server (`packages/core`), which in turn manages requests to the Gemini API and its AI models. Gemini CLI also contains a variety of tools for tasks such as performing file system operations, running shells, and web fetching, which are managed by `packages/core`.
|
||||
|
||||
## Navigating the documentation
|
||||
|
||||
This documentation is organized into the following sections:
|
||||
|
||||
- **[Execution and Deployment](./deployment.md):** Information for running Gemini CLI.
|
||||
- **[Architecture Overview](./architecture.md):** Understand the high-level design of Gemini CLI, including its components and how they interact.
|
||||
- **CLI Usage:** Documentation for `packages/cli`.
|
||||
- **[CLI Introduction](./cli/index.md):** Overview of the command-line interface.
|
||||
- **[Commands](./cli/commands.md):** Description of available CLI commands.
|
||||
- **[Configuration](./cli/configuration.md):** Information on configuring the CLI.
|
||||
- **[Extensions](./extension.md):** How to extend the CLI with new functionality.
|
||||
- **Core Details:** Documentation for `packages/core`.
|
||||
- **[Core Introduction](./core/index.md):** Overview of the core component.
|
||||
- **[Configuration](./core/configuration.md):** Information on configuring the core component.
|
||||
- **[Tools API](./core/tools-api.md):** Information on how the core manages and exposes tools.
|
||||
- **Tools:**
|
||||
- **[Tools Overview](./tools/index.md):** Overview of the available tools.
|
||||
- **[File System Tools](./tools/file-system.md):** Documentation for the `read_file` and `write_file` tools.
|
||||
- **[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` tool.
|
||||
- **[Web Search Tool](./tools/web-search.md):** Documentation for the `google_web_search` tool.
|
||||
- **[Contributing & Development Guide](../CONTRIBUTING.md):** Information for contributors and developers, including setup, building, testing, and coding conventions.
|
||||
- **[Troubleshooting Guide](./troubleshooting.md):** Find solutions to common problems and FAQs.
|
||||
|
||||
We hope this documentation helps you make the most of the Gemini CLI!
|
||||
@@ -0,0 +1,141 @@
|
||||
# Integration Tests
|
||||
|
||||
This document provides information about the integration testing framework used in this project.
|
||||
|
||||
## Overview
|
||||
|
||||
The integration tests are designed to validate the end-to-end functionality of the Gemini CLI. They execute the built binary in a controlled environment and verify that it behaves as expected when interacting with the file system.
|
||||
|
||||
These tests are located in the `integration-tests` directory and are run using a custom test runner.
|
||||
|
||||
## Running the tests
|
||||
|
||||
The integration tests are not run as part of the default `npm run test` command. They must be run explicitly using the `npm run test:integration:all` script.
|
||||
|
||||
The integration tests can also be run using the following shortcut:
|
||||
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## Running a specfic set of tests
|
||||
|
||||
To run a subset of test files, you can use `npm run <integration test command> <file_name1> ....` where <integration test command> is either `test:e2e` or `test:integration*` and `<file_name>` is any of the `.test.js` files in the `integration-tests/` directory. For example, the following command runs `list_directory.test.js` and `write_file.test.js`:
|
||||
|
||||
```bash
|
||||
npm run test:e2e list_directory write_file
|
||||
```
|
||||
|
||||
### Running a single test by name
|
||||
|
||||
To run a single test by its name, use the `--test-name-pattern` flag:
|
||||
|
||||
```bash
|
||||
npm run test:e2e -- --test-name-pattern "reads a file"
|
||||
```
|
||||
|
||||
### Running all tests
|
||||
|
||||
To run the entire suite of integration tests, use the following command:
|
||||
|
||||
```bash
|
||||
npm run test:integration:all
|
||||
```
|
||||
|
||||
### Sandbox matrix
|
||||
|
||||
The `all` command will run tests for `no sandboxing`, `docker` and `podman`.
|
||||
Each individual type can be run using the following commands:
|
||||
|
||||
```bash
|
||||
npm run test:integration:sandbox:none
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run test:integration:sandbox:docker
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run test:integration:sandbox:podman
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
|
||||
The integration test runner provides several options for diagnostics to help track down test failures.
|
||||
|
||||
### Keeping test output
|
||||
|
||||
You can preserve the temporary files created during a test run for inspection. This is useful for debugging issues with file system operations.
|
||||
|
||||
To keep the test output, you can either use the `--keep-output` flag or set the `KEEP_OUTPUT` environment variable to `true`.
|
||||
|
||||
```bash
|
||||
# Using the flag
|
||||
npm run test:integration:sandbox:none -- --keep-output
|
||||
|
||||
# Using the environment variable
|
||||
KEEP_OUTPUT=true npm run test:integration:sandbox:none
|
||||
```
|
||||
|
||||
When output is kept, the test runner will print the path to the unique directory for the test run.
|
||||
|
||||
### Verbose output
|
||||
|
||||
For more detailed debugging, the `--verbose` flag streams the real-time output from the `gemini` command to the console.
|
||||
|
||||
```bash
|
||||
npm run test:integration:sandbox:none -- --verbose
|
||||
```
|
||||
|
||||
When using `--verbose` and `--keep-output` in the same command, the output is streamed to the console and also saved to a log file within the test's temporary directory.
|
||||
|
||||
The verbose output is formatted to clearly identify the source of the logs:
|
||||
|
||||
```
|
||||
--- TEST: <file-name-without-js>:<test-name> ---
|
||||
... output from the gemini command ...
|
||||
--- END TEST: <file-name-without-js>:<test-name> ---
|
||||
```
|
||||
|
||||
## Linting and formatting
|
||||
|
||||
To ensure code quality and consistency, the integration test files are linted as part of the main build process. You can also manually run the linter and auto-fixer.
|
||||
|
||||
### Running the linter
|
||||
|
||||
To check for linting errors, run the following command:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
You can include the `--fix` flag in the command to automatically fix any fixable linting errors:
|
||||
|
||||
```bash
|
||||
npm run lint --fix
|
||||
```
|
||||
|
||||
## Directory structure
|
||||
|
||||
The integration tests create a unique directory for each test run inside the `.integration-tests` directory. Within this directory, a subdirectory is created for each test file, and within that, a subdirectory is created for each individual test case.
|
||||
|
||||
This structure makes it easy to locate the artifacts for a specific test run, file, or case.
|
||||
|
||||
```
|
||||
.integration-tests/
|
||||
└── <run-id>/
|
||||
└── <test-file-name>.test.js/
|
||||
└── <test-case-name>/
|
||||
├── output.log
|
||||
└── ...other test artifacts...
|
||||
```
|
||||
|
||||
## Continuous integration
|
||||
|
||||
To ensure the integration tests are always run, a GitHub Actions workflow is defined in `.github/workflows/e2e.yml`. This workflow automatically runs the integration tests on every pull request and push to the `main` branch.
|
||||
|
||||
The workflow runs the tests in different sandboxing environments to ensure Gemini CLI is tested across each:
|
||||
|
||||
- `sandbox:none`: Runs the tests without any sandboxing.
|
||||
- `sandbox:docker`: Runs the tests in a Docker container.
|
||||
- `sandbox:podman`: Runs the tests in a Podman container.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Gemini CLI: File System Tools
|
||||
|
||||
The Gemini CLI provides a comprehensive suite of tools for interacting with the local file system. These tools allow the Gemini model to read from, write to, list, search, and modify files and directories, all under your control and typically with confirmation for sensitive operations.
|
||||
|
||||
All file system tools operate within a `rootDirectory` (usually the current working directory where you launched the CLI) for security, preventing unintended access to other parts of your system. Paths provided to these tools are generally expected to be absolute or are resolved relative to this root directory.
|
||||
|
||||
## 1. `list_directory` (ReadFolder)
|
||||
|
||||
- **Tool Name:** `list_directory`
|
||||
- **Display Name:** ReadFolder
|
||||
- **File:** `ls.ts`
|
||||
- **Description:** Lists the names of files and subdirectories directly within a specified directory path. It can optionally ignore entries matching provided glob patterns.
|
||||
- **Parameters:**
|
||||
- `path` (string, required): The absolute path to the directory to list.
|
||||
- `ignore` (array of strings, optional): A list of glob patterns to exclude from the listing (e.g., `["*.log", ".git"]`).
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when listing files. Defaults to true.
|
||||
- **Behavior:**
|
||||
- Returns a list of file and directory names.
|
||||
- Indicates whether each entry is a directory.
|
||||
- Sorts entries with directories first, then alphabetically.
|
||||
- **Output (`llmContent`):** A string like: `Directory listing for /path/to/your/folder:\n[DIR] subfolder1\nfile1.txt\nfile2.png`
|
||||
- **Confirmation:** No.
|
||||
|
||||
## 2. `read_file` (ReadFile)
|
||||
|
||||
- **Tool Name:** `read_file`
|
||||
- **Display Name:** ReadFile
|
||||
- **File:** `read-file.ts`
|
||||
- **Description:** Reads and returns the content of a specified file. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges. Other binary file types are generally skipped.
|
||||
- **Parameters:**
|
||||
- `path` (string, required): The absolute path to the file to read.
|
||||
- `offset` (number, optional): For text files, the 0-based line number to start reading from. Requires `limit` to be set.
|
||||
- `limit` (number, optional): For text files, the maximum number of lines to read. If omitted, reads a default maximum (e.g., 2000 lines) or the entire file if feasible.
|
||||
- **Behavior:**
|
||||
- For text files: Returns the content. If `offset` and `limit` are used, returns only that slice of lines. Indicates if content was truncated due to line limits or line length limits.
|
||||
- For image and PDF files: Returns the file content as a base64 encoded data structure suitable for model consumption.
|
||||
- For other binary files: Attempts to identify and skip them, returning a message indicating it's a generic binary file.
|
||||
- **Output:** (`llmContent`):
|
||||
- For text files: The file content, potentially prefixed with a truncation message (e.g., `[File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...`).
|
||||
- For image/PDF files: An object containing `inlineData` with `mimeType` and base64 `data` (e.g., `{ inlineData: { mimeType: 'image/png', data: 'base64encodedstring' } }`).
|
||||
- For other binary files: A message like `Cannot display content of binary file: /path/to/data.bin`.
|
||||
- **Confirmation:** No.
|
||||
|
||||
## 3. `write_file` (WriteFile)
|
||||
|
||||
- **Tool Name:** `write_file`
|
||||
- **Display Name:** WriteFile
|
||||
- **File:** `write-file.ts`
|
||||
- **Description:** Writes content to a specified file. If the file exists, it will be overwritten. If it doesn't exist, it (and any necessary parent directories) will be created.
|
||||
- **Parameters:**
|
||||
- `file_path` (string, required): The absolute path to the file to write to.
|
||||
- `content` (string, required): The content to write into the file.
|
||||
- **Behavior:**
|
||||
- Writes the provided `content` to the `file_path`.
|
||||
- Creates parent directories if they don't exist.
|
||||
- **Output (`llmContent`):** A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt` or `Successfully created and wrote to new file: /path/to/new/file.txt`.
|
||||
- **Confirmation:** Yes. Shows a diff of changes and asks for user approval before writing.
|
||||
|
||||
## 4. `glob` (FindFiles)
|
||||
|
||||
- **Tool Name:** `glob`
|
||||
- **Display Name:** FindFiles
|
||||
- **File:** `glob.ts`
|
||||
- **Description:** Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `*.md`), returning absolute paths sorted by modification time (newest first).
|
||||
- **Parameters:**
|
||||
- `pattern` (string, required): The glob pattern to match against (e.g., `"*.py"`, `"src/**/*.js"`).
|
||||
- `path` (string, optional): The absolute path to the directory to search within. If omitted, searches the tool's root directory.
|
||||
- `case_sensitive` (boolean, optional): Whether the search should be case-sensitive. Defaults to false.
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to true.
|
||||
- **Behavior:**
|
||||
- Searches for files matching the glob pattern within the specified directory.
|
||||
- Returns a list of absolute paths, sorted with the most recently modified files first.
|
||||
- Ignores common nuisance directories like `node_modules` and `.git` by default.
|
||||
- **Output (`llmContent`):** A message like: `Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...`
|
||||
- **Confirmation:** No.
|
||||
|
||||
## 5. `search_file_content` (SearchText)
|
||||
|
||||
- **Tool Name:** `search_file_content`
|
||||
- **Display Name:** SearchText
|
||||
- **File:** `grep.ts`
|
||||
- **Description:** Searches for a regular expression pattern within the content of files in a specified directory. Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.
|
||||
- **Parameters:**
|
||||
- `pattern` (string, required): The regular expression (regex) to search for (e.g., `"function\s+myFunction"`).
|
||||
- `path` (string, optional): The absolute path to the directory to search within. Defaults to the current working directory.
|
||||
- `include` (string, optional): A glob pattern to filter which files are searched (e.g., `"*.js"`, `"src/**/*.{ts,tsx}"`). If omitted, searches most files (respecting common ignores).
|
||||
- **Behavior:**
|
||||
- Uses `git grep` if available in a Git repository for speed, otherwise falls back to system `grep` or a JavaScript-based search.
|
||||
- Returns a list of matching lines, each prefixed with its file path (relative to the search directory) and line number.
|
||||
- **Output (`llmContent`):** A formatted string of matches, e.g.:
|
||||
```
|
||||
Found 3 match(es) for pattern "myFunction" in path "." (filter: "*.ts"):
|
||||
---
|
||||
File: src/utils.ts
|
||||
L15: export function myFunction() {
|
||||
L22: myFunction.call();
|
||||
---
|
||||
File: src/index.ts
|
||||
L5: import { myFunction } from './utils';
|
||||
---
|
||||
```
|
||||
- **Confirmation:** No.
|
||||
|
||||
## 6. `replace` (Edit)
|
||||
|
||||
- **Tool Name:** `replace`
|
||||
- **Display Name:** Edit
|
||||
- **File:** `edit.ts`
|
||||
- **Description:** Replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when `expected_replacements` is specified. This tool is designed for precise, targeted changes and requires significant context around the `old_string` to ensure it modifies the correct location.
|
||||
- **Parameters:**
|
||||
- `file_path` (string, required): The absolute path to the file to modify.
|
||||
- `old_string` (string, required): The exact literal text to replace. **CRITICAL:** This string must uniquely identify the single instance to change. It should include at least 3 lines of context _before_ and _after_ the target text, matching whitespace and indentation precisely. If `old_string` is empty, the tool attempts to create a new file at `file_path` with `new_string` as content.
|
||||
- `new_string` (string, required): The exact literal text to replace `old_string` with.
|
||||
- `expected_replacements` (number, optional): The number of occurrences to replace. Defaults to 1.
|
||||
- **Behavior:**
|
||||
- If `old_string` is empty and `file_path` does not exist, creates a new file with `new_string` as content.
|
||||
- If `old_string` is provided, it reads the `file_path` and attempts to find exactly one occurrence of `old_string`.
|
||||
- If one occurrence is found, it replaces it with `new_string`.
|
||||
- **Enhanced Reliability (Multi-Stage Edit Correction):** To significantly improve the success rate of edits, especially when the model-provided `old_string` might not be perfectly precise, the tool incorporates a multi-stage edit correction mechanism.
|
||||
- If the initial `old_string` isn't found or matches multiple locations, the tool can leverage the Gemini model to iteratively refine `old_string` (and potentially `new_string`).
|
||||
- This self-correction process attempts to identify the unique segment the model intended to modify, making the `replace` operation more robust even with slightly imperfect initial context from the AI.
|
||||
- **Failure Conditions:** Despite the correction mechanism, the tool will fail if:
|
||||
- `file_path` is not absolute or is outside the root directory.
|
||||
- `old_string` is not empty, but the `file_path` does not exist.
|
||||
- `old_string` is empty, but the `file_path` already exists.
|
||||
- `old_string` is not found in the file after attempts to correct it.
|
||||
- `old_string` is found multiple times, and the self-correction mechanism cannot resolve it to a single, unambiguous match.
|
||||
- **Output (`llmContent`):**
|
||||
- On success: `Successfully modified file: /path/to/file.txt (1 replacements).` or `Created new file: /path/to/new_file.txt with provided content.`
|
||||
- On failure: An error message explaining the reason (e.g., `Failed to edit, 0 occurrences found...`, `Failed to edit, expected 1 occurrences but found 2...`).
|
||||
- **Confirmation:** Yes. Shows a diff of the proposed changes and asks for user approval before writing to the file.
|
||||
|
||||
These file system tools provide a robust foundation for the Gemini CLI to understand and interact with your local project context.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Gemini CLI Tools: Overview
|
||||
|
||||
The Gemini CLI is equipped with a powerful set of built-in tools that the Gemini model can utilize to interact with your local environment, access information, and perform actions. These tools significantly enhance the CLI's capabilities, allowing it to go beyond text generation and assist with a wide range of tasks.
|
||||
|
||||
## What are Tools?
|
||||
|
||||
In the context of the Gemini CLI, tools are specific functions or modules that the Gemini model can request to be executed. For example, if you ask Gemini to "Summarize the contents of `my_document.txt`," the model will likely identify the need to read that file and will request the execution of the `read_file` tool.
|
||||
|
||||
The core component (`packages/core`) manages these tools, presents their definitions (schemas) to the Gemini model, executes them when requested, and returns the results to the model for further processing into a user-facing response.
|
||||
|
||||
## Why are Tools Important?
|
||||
|
||||
- **Access to Local Information:** Tools allow Gemini to access your local file system, read file contents, list directories, etc.
|
||||
- **Execution of Commands:** With tools like `run_shell_command`, Gemini can run shell commands (with appropriate safety measures and user confirmation).
|
||||
- **Interaction with the Web:** Tools can fetch content from URLs.
|
||||
- **Action Taking:** Tools can modify files, write new files, or perform other actions on your system (again, typically with safeguards).
|
||||
- **Grounding Responses:** By using tools to fetch real-time or specific local data, Gemini's responses can be more accurate, relevant, and grounded in your actual context.
|
||||
|
||||
## How Tools are Used
|
||||
|
||||
1. You provide a prompt to the Gemini CLI.
|
||||
2. The CLI sends the prompt to the core.
|
||||
3. The core, along with your prompt and conversation history, sends a list of available tools and their descriptions/schemas to the Gemini API.
|
||||
4. The Gemini model analyzes your request. If it determines that a tool is needed, its response will include a request to execute a specific tool with certain parameters.
|
||||
5. The core receives this tool request, validates it, and (often after user confirmation for sensitive operations) executes the tool.
|
||||
6. The output from the tool is sent back to the Gemini model.
|
||||
7. The Gemini model uses the tool's output to formulate its final answer, which is then sent back through the core to the CLI and displayed to you.
|
||||
|
||||
You will typically see messages in the CLI indicating when a tool is being called and whether it succeeded or failed.
|
||||
|
||||
## Security and Confirmation
|
||||
|
||||
Many tools, especially those that can modify your file system or execute commands (`write_file`, `edit`, `run_shell_command`), are designed with safety in mind. The Gemini CLI will typically:
|
||||
|
||||
- **Require Confirmation:** Prompt you before executing potentially sensitive operations, showing you what action is about to be taken.
|
||||
- **Utilize Sandboxing:** All tools are subject to restrictions enforced by sandboxing (see [README](../../README.md#sandboxing)). This means that when operating in a sandbox, any tools (including MCP servers) you wish to use must be available _inside_ the sandbox environment. For example, to run an MCP server through `npx`, the `npx` executable must be installed within the sandbox's Docker image or be available in the `sandbox-exec` environment.
|
||||
|
||||
It's important to always review confirmation prompts carefully before allowing a tool to proceed.
|
||||
|
||||
## Categories of Built-in Tools
|
||||
|
||||
The built-in tools can be broadly categorized as follows:
|
||||
|
||||
- **[File System Tools](./file-system.md):** For interacting with files and directories (reading, writing, listing, searching, etc.).
|
||||
- **[Shell Tool](./shell.md):** For executing shell commands.
|
||||
- **[Web Fetch Tool](./web-fetch.md):** For retrieving content from URLs.
|
||||
- **[Web Search Tool](./web-search.md):** For searching the web.
|
||||
- **[Multi-File Read Tool](./multi-file.md):** A specialized tool for reading content from multiple files or directories, often used by the `@` command.
|
||||
- **[Memory Tool](./memory.md):** For saving and recalling information across sessions.
|
||||
|
||||
Understanding the available tools and how they work will help you make the most effective use of the Gemini CLI.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Memory Tool
|
||||
|
||||
This document provides details on the `save_memory` tool.
|
||||
|
||||
## `save_memory`
|
||||
|
||||
- **Purpose:** Saves a specific piece of information or fact to your long-term memory. This allows the CLI to remember key details across sessions, providing more personalized and effective assistance.
|
||||
- **Arguments:**
|
||||
- `fact` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement.
|
||||
- **Behavior:**
|
||||
- The tool appends the provided `fact` to a special `GEMINI.md` file located in the user's home directory (`~/.gemini/GEMINI.md`). This file can be configured to have a different name.
|
||||
- The facts are stored under a `## Gemini Added Memories` section.
|
||||
- This file is loaded as context in subsequent sessions, allowing the CLI to recall the saved information.
|
||||
- **Examples:**
|
||||
- Remembering a user preference:
|
||||
```
|
||||
save_memory(fact="My preferred programming language is Python.")
|
||||
```
|
||||
- Storing a project-specific detail:
|
||||
```
|
||||
save_memory(fact="The project I'm currently working on is called 'gemini-cli'.")
|
||||
```
|
||||
- **Important Notes:**
|
||||
- This tool should be used for concise, important facts. It is not intended for storing large amounts of data or conversational history.
|
||||
- The memory file is a plain text Markdown file, so you can view and edit it manually if needed.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Multi-File Read Tool
|
||||
|
||||
This document provides details on the `read_many_files` tool.
|
||||
|
||||
## `read_many_files`
|
||||
|
||||
- **Purpose:** Reads content from multiple files specified by paths or glob patterns. For text files, it concatenates their content into a single string. For image (e.g., PNG, JPEG) and PDF files, it reads and returns them as base64 encoded data, provided they are explicitly requested by name or extension. This is useful for getting an overview of a codebase, finding where specific functionality is implemented, reviewing documentation, or gathering context from multiple configuration files.
|
||||
- **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.
|
||||
- **Behavior:**
|
||||
- The tool 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.
|
||||
- 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.
|
||||
- **Examples:**
|
||||
- Reading all TypeScript files in the `src` directory:
|
||||
```
|
||||
read_many_files(paths=["src/**/*.ts"])
|
||||
```
|
||||
- Reading 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"])
|
||||
```
|
||||
- Reading all JavaScript files but explicitly including 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 Files:** The tool can read common image types (PNG, JPEG, etc.) and PDF 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 `image.png` or a pattern like `*.jpeg`).
|
||||
- **Other Binary Files:** The tool attempts to detect and skip other types of binary files by examining their initial content for null bytes. Its behavior with such files is to exclude them from the 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.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Shell Tool
|
||||
|
||||
This document provides details on the shell tool.
|
||||
|
||||
## `run_shell_command`
|
||||
|
||||
- **Purpose:** Executes a given shell command. On Windows, this will be executed with `cmd.exe /c`. On other platforms, it will be executed with `bash -c`. This tool is essential for interacting with the underlying operating system, running scripts, or performing command-line operations.
|
||||
- **Arguments:**
|
||||
- `command` (string, required): The exact shell command to execute.
|
||||
- `description` (string, optional): A brief description of the command's purpose, which will be shown to the user.
|
||||
- `directory` (string, optional): The directory (relative to the project root) in which to execute the command. If not provided, the command runs in the project root.
|
||||
- **Behavior:**
|
||||
- The command is executed as a subprocess.
|
||||
- It can start background processes using `&`.
|
||||
- The tool returns detailed information about the execution, including:
|
||||
- `Command`: The command that was executed.
|
||||
- `Directory`: The directory where the command was run.
|
||||
- `Stdout`: Output from the standard output stream.
|
||||
- `Stderr`: Output from the standard error stream.
|
||||
- `Error`: Any error message reported by the subprocess.
|
||||
- `Exit Code`: The exit code of the command.
|
||||
- `Signal`: The signal number if the command was terminated by a signal.
|
||||
- `Background PIDs`: A list of PIDs for any background processes started.
|
||||
- **Examples:**
|
||||
|
||||
- Listing files in the current directory:
|
||||
|
||||
```
|
||||
|
||||
run_shell_command(command="ls -la")
|
||||
```
|
||||
|
||||
- Running a script in a specific directory:
|
||||
```
|
||||
run_shell_command(command="./my_script.sh", directory="scripts", description="Run my custom script")
|
||||
```
|
||||
- Starting a background server:
|
||||
```
|
||||
run_shell_command(command="npm run dev &", description="Start development server in background")
|
||||
```
|
||||
|
||||
- **Important Notes:**
|
||||
- **Security:** Be cautious when executing commands, especially those constructed from user input, to prevent security vulnerabilities.
|
||||
- **Interactive Commands:** Avoid commands that require interactive user input, as this can cause the tool to hang. Use non-interactive flags if available (e.g., `npm init -y`).
|
||||
- **Error Handling:** Check the `Stderr`, `Error`, and `Exit Code` fields to determine if a command executed successfully.
|
||||
- **Background Processes:** When a command is run in the background with `&`, the tool will return immediately and the process will continue to run in the background. The `Background PIDs` field will contain the process ID of the background process.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Web Fetch Tool
|
||||
|
||||
This document describes the `web_fetch` tool.
|
||||
|
||||
## `web_fetch`
|
||||
|
||||
- **Purpose:** Processes content from one or more URLs (up to 20) embedded in a prompt. This tool is ideal for tasks that require summarizing, comparing, or extracting specific information from web pages.
|
||||
- **Arguments:**
|
||||
- `prompt` (string, required): A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content. For example: `"Summarize https://example.com/article and extract key points from https://another.com/data"`. The prompt must contain at least one URL starting with `http://` or `https://`.
|
||||
- **Behavior:**
|
||||
- The tool will first ask for confirmation before fetching any URLs.
|
||||
- It attempts to process URLs through the Gemini API's `urlContext` tool first.
|
||||
- If the Gemini API cannot access a URL (e.g., it's on `localhost`, a private network, or behind a firewall), the tool will fall back to fetching the content directly from the local machine.
|
||||
- The tool can fetch content from `localhost` and private network addresses via this fallback mechanism.
|
||||
- The tool formats the response, including source attribution with citations where possible, and returns it to the user.
|
||||
- **Examples:**
|
||||
- Summarizing a single article:
|
||||
```
|
||||
web_fetch(prompt="Can you summarize the main points of https://example.com/news/latest")
|
||||
```
|
||||
- Comparing two articles:
|
||||
```
|
||||
web_fetch(prompt="What are the differences in the conclusions of these two papers: https://arxiv.org/abs/2401.0001 and https://arxiv.org/abs/2401.0002?")
|
||||
```
|
||||
- **Important Notes:**
|
||||
- This tool relies on the Gemini API's ability to access and process the given URLs.
|
||||
- The quality of the output depends on the clarity of the instructions in the prompt.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Google Web Search Tool
|
||||
|
||||
This document describes the `google_web_search` tool.
|
||||
|
||||
## `google_web_search`
|
||||
|
||||
- **Purpose:** Performs a web search using Google Search (via the Gemini API) and returns a summary of the results with sources. This tool is useful for finding up-to-date information on the internet.
|
||||
- **Arguments:**
|
||||
- `query` (string, required): The search query.
|
||||
- **Behavior:**
|
||||
- The tool sends the query to the Gemini API, which performs a Google Search.
|
||||
- It returns a generated response based on the search results, complete with citations and a list of sources.
|
||||
- **Examples:**
|
||||
- Getting information on a topic:
|
||||
```
|
||||
google_web_search(query="latest advancements in AI-powered code generation")
|
||||
```
|
||||
- **Important Notes:**
|
||||
- The tool returns a processed summary, not a raw list of search results.
|
||||
- The response includes citations to the sources used to generate the summary.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
This guide provides solutions to common issues and debugging tips.
|
||||
|
||||
## Frequently asked questions (FAQs)
|
||||
|
||||
- **Q: How do I update Gemini CLI to the latest version?**
|
||||
|
||||
- A: If installed globally via npm, update Gemini CLI using the command `npm install -g @gemini-cli/cli@latest`. If run from source, pull the latest changes from the repository and rebuild using `npm run build`.
|
||||
|
||||
- **Q: Where are Gemini CLI configuration files stored?**
|
||||
|
||||
- A: The CLI configuration is stored within two `settings.json` files: one in your home directory and one in your project's root directory. In both locations, `settings.json` is found in the `.gemini/` folder. Refer to [CLI Configuration](./cli/configuration.md) for more details.
|
||||
|
||||
## Common error messages and solutions
|
||||
|
||||
- **Error: `EADDRINUSE` (Address already in use) when starting an MCP server.**
|
||||
|
||||
- **Cause:** Another process is already using the port the MCP server is trying to bind to.
|
||||
- **Solution:**
|
||||
Either stop the other process that is using the port or configure the MCP server to use a different port.
|
||||
|
||||
- **Error: Command not found (when attempting the run Gemini CLI).**
|
||||
|
||||
- **Cause:** Gemini CLI is not correctly installed or not in your system's PATH.
|
||||
- **Solution:**
|
||||
1. Ensure Gemini CLI installation was successful.
|
||||
2. If installed globally, check that your npm global binary directory is in your PATH.
|
||||
3. If running from source, ensure you are using the correct command to invoke it (e.g., `node packages/cli/dist/index.js ...`).
|
||||
|
||||
- **Error: `MODULE_NOT_FOUND` or import errors.**
|
||||
|
||||
- **Cause:** Dependencies are not installed correctly, or the project hasn't been built.
|
||||
- **Solution:**
|
||||
1. Run `npm install` to ensure all dependencies are present.
|
||||
2. Run `npm run build` to compile the project.
|
||||
|
||||
- **Error: "Operation not permitted", "Permission denied", or similar.**
|
||||
|
||||
- **Cause:** If sandboxing is enabled, then the application is likely attempting an operation restricted by your sandbox, such as writing outside the project directory or system temp directory.
|
||||
- **Solution:** See [Sandboxing](./cli/configuration.md#sandboxing) for more information, including how to customize your sandbox configuration.
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
- **CLI debugging:**
|
||||
|
||||
- Use the `--verbose` flag (if available) with CLI commands for more detailed output.
|
||||
- Check the CLI logs, often found in a user-specific configuration or cache directory.
|
||||
|
||||
- **Core debugging:**
|
||||
|
||||
- Check the server console output for error messages or stack traces.
|
||||
- Increase log verbosity if configurable.
|
||||
- Use Node.js debugging tools (e.g., `node --inspect`) if you need to step through server-side code.
|
||||
|
||||
- **Tool issues:**
|
||||
|
||||
- If a specific tool is failing, try to isolate the issue by running the simplest possible version of the command or operation the tool performs.
|
||||
- For `run_shell_command`, check that the command works directly in your shell first.
|
||||
- For file system tools, double-check paths and permissions.
|
||||
|
||||
- **Pre-flight checks:**
|
||||
- Always run `npm run preflight` before committing code. This can catch many common issues related to formatting, linting, and type errors.
|
||||
|
||||
If you encounter an issue not covered here, consider searching the project's issue tracker on GitHub or reporting a new issue with detailed information.
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkg = require(path.resolve(__dirname, 'package.json'));
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
entryPoints: ['packages/cli/index.ts'],
|
||||
bundle: true,
|
||||
outfile: 'bundle/gemini.js',
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
define: {
|
||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||
},
|
||||
banner: {
|
||||
js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url); globalThis.__filename = require('url').fileURLToPath(import.meta.url); globalThis.__dirname = require('path').dirname(globalThis.__filename);`,
|
||||
},
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Disallows relative imports between specified monorepo packages.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
/**
|
||||
* Finds the package name by searching for the nearest `package.json` file
|
||||
* in the directory hierarchy, starting from the given file's directory
|
||||
* and moving upwards until the specified root directory is reached.
|
||||
* It reads the `package.json` and extracts the `name` property.
|
||||
*
|
||||
* @requires module:path Node.js path module
|
||||
* @requires module:fs Node.js fs module
|
||||
*
|
||||
* @param {string} filePath - The path (absolute or relative) to a file within the potential package structure.
|
||||
* The search starts from the directory containing this file.
|
||||
* @param {string} root - The absolute path to the root directory of the project/monorepo.
|
||||
* The upward search stops when this directory is reached.
|
||||
* @returns {string | undefined | null} The value of the `name` field from the first `package.json` found.
|
||||
* Returns `undefined` if the `name` field doesn't exist in the found `package.json`.
|
||||
* Returns `null` if no `package.json` is found before reaching the `root` directory.
|
||||
* @throws {Error} Can throw an error if `fs.readFileSync` fails (e.g., permissions) or if `JSON.parse` fails on invalid JSON content.
|
||||
*/
|
||||
function findPackageName(filePath, root) {
|
||||
let currentDir = path.dirname(path.resolve(filePath));
|
||||
while (currentDir !== root) {
|
||||
const parentDir = path.dirname(currentDir);
|
||||
const packageJsonPath = path.join(currentDir, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
return pkg.name;
|
||||
}
|
||||
|
||||
// Move up one level
|
||||
currentDir = parentDir;
|
||||
// Safety break if we somehow reached the root directly in the loop condition (less likely with path.resolve)
|
||||
if (path.dirname(currentDir) === currentDir) break;
|
||||
}
|
||||
|
||||
return null; // Not found within the expected structure
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow relative imports between packages.',
|
||||
category: 'Best Practices',
|
||||
recommended: 'error',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
root: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Absolute path to the root of all relevant packages to consider.',
|
||||
},
|
||||
},
|
||||
required: ['root'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
noRelativePathsForCrossPackageImport:
|
||||
"Relative import '{{importedPath}}' crosses package boundary from '{{importingPackage}}' to '{{importedPackage}}'. Use a direct package import ('{{importedPackage}}') instead.",
|
||||
relativeImportIsInvalidPackage:
|
||||
"Relative import '{{importedPath}}' does not reference a valid package. All source must be in a package directory.",
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const allPackagesRoot = options.root;
|
||||
|
||||
const currentFilePath = context.filename;
|
||||
if (
|
||||
!currentFilePath ||
|
||||
currentFilePath === '<input>' ||
|
||||
currentFilePath === '<text>'
|
||||
) {
|
||||
// Skip if filename is not available (e.g., linting raw text)
|
||||
return {};
|
||||
}
|
||||
|
||||
const currentPackage = findPackageName(currentFilePath, allPackagesRoot);
|
||||
|
||||
// If the current file isn't inside a package structure, don't apply the rule
|
||||
if (!currentPackage) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importingPackage = currentPackage;
|
||||
const importedPath = node.source.value;
|
||||
|
||||
// Only interested in relative paths
|
||||
if (
|
||||
!importedPath ||
|
||||
typeof importedPath !== 'string' ||
|
||||
!importedPath.startsWith('.')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the absolute path of the imported module
|
||||
const absoluteImportPath = path.resolve(
|
||||
path.dirname(currentFilePath),
|
||||
importedPath,
|
||||
);
|
||||
|
||||
// Find the package information for the imported file
|
||||
const importedPackage = findPackageName(
|
||||
absoluteImportPath,
|
||||
allPackagesRoot,
|
||||
);
|
||||
|
||||
// If the imported file isn't in a recognized package, report issue
|
||||
if (!importedPackage) {
|
||||
context.report({
|
||||
node: node.source,
|
||||
messageId: 'relativeImportIsInvalidPackage',
|
||||
data: { importedPath: importedPath },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// The core check: Are the source and target packages different?
|
||||
if (currentPackage !== importedPackage) {
|
||||
// We found a relative import crossing package boundaries
|
||||
context.report({
|
||||
node: node.source, // Report the error on the source string literal
|
||||
messageId: 'noRelativePathsForCrossPackageImport',
|
||||
data: {
|
||||
importedPath,
|
||||
importedPackage,
|
||||
importingPackage,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node.source, `'${importedPackage}'`);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import globals from 'globals';
|
||||
import licenseHeader from 'eslint-plugin-license-header';
|
||||
import noRelativeCrossPackageImports from './eslint-rules/no-relative-cross-package-imports.js';
|
||||
import path from 'node:path'; // Use node: prefix for built-ins
|
||||
import url from 'node:url';
|
||||
|
||||
// --- ESM way to get __dirname ---
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
// --- ---
|
||||
|
||||
// Determine the monorepo root (assuming eslint.config.js is at the root)
|
||||
const projectRoot = __dirname;
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
// Global ignores
|
||||
ignores: [
|
||||
'node_modules/*',
|
||||
'eslint.config.js',
|
||||
'packages/cli/dist/**',
|
||||
'packages/core/dist/**',
|
||||
'packages/server/dist/**',
|
||||
'eslint-rules/*',
|
||||
'bundle/**',
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactPlugin.configs.flat.recommended,
|
||||
reactPlugin.configs.flat['jsx-runtime'], // Add this if you are using React 17+
|
||||
{
|
||||
// Settings for eslint-plugin-react
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Import specific config
|
||||
files: ['packages/cli/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...importPlugin.configs.recommended.rules,
|
||||
...importPlugin.configs.typescript.rules,
|
||||
'import/no-default-export': 'warn',
|
||||
'import/no-unresolved': 'off', // Disable for now, can be noisy with monorepos/paths
|
||||
},
|
||||
},
|
||||
{
|
||||
// General overrides and rules for the project (TS/TSX files)
|
||||
files: ['packages/*/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.es2021,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// General Best Practice Rules (subset adapted for flat config)
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
curly: ['error', 'multi-line'],
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{ assertionStyle: 'as' },
|
||||
],
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'error',
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-inferrable-types': [
|
||||
'error',
|
||||
{ ignoreParameters: true, ignoreProperties: true },
|
||||
],
|
||||
'@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'no-cond-assign': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'CallExpression[callee.name="require"]',
|
||||
message: 'Avoid using require(). Use ES6 imports instead.',
|
||||
},
|
||||
{
|
||||
selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
|
||||
message:
|
||||
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
|
||||
},
|
||||
],
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unused-expressions': 'off', // Disable base rule
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
// Enable TS version
|
||||
'error',
|
||||
{ allowShortCircuit: true, allowTernary: true },
|
||||
],
|
||||
'no-var': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'one-var': ['error', 'never'],
|
||||
'prefer-arrow-callback': 'error',
|
||||
'prefer-const': ['error', { destructuring: 'all' }],
|
||||
radix: 'error',
|
||||
'default-case': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./**/*.{tsx,ts,js}'],
|
||||
plugins: {
|
||||
'license-header': licenseHeader,
|
||||
},
|
||||
rules: {
|
||||
'license-header/header': [
|
||||
'error',
|
||||
[
|
||||
'/**',
|
||||
' * @license',
|
||||
' * Copyright 2025 Google LLC',
|
||||
' * SPDX-License-Identifier: Apache-2.0',
|
||||
' */',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
// extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['./scripts/**/*.js', 'esbuild.config.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// Prettier config must be last
|
||||
prettierConfig,
|
||||
// extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['./integration-tests/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// Custom eslint rules for this repo
|
||||
{
|
||||
files: ['packages/**/*.{js,jsx,ts,tsx}'],
|
||||
plugins: {
|
||||
custom: {
|
||||
rules: {
|
||||
'no-relative-cross-package-imports': noRelativeCrossPackageImports,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Enable and configure your custom rule
|
||||
'custom/no-relative-cross-package-imports': [
|
||||
'error',
|
||||
{
|
||||
root: path.join(projectRoot, 'packages'),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { strict as assert } from 'assert';
|
||||
import { test } from 'node:test';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
test('reads a file', (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
rig.createFile('test.txt', 'hello world');
|
||||
|
||||
const output = rig.run(`read the file name test.txt`);
|
||||
|
||||
assert.ok(output.toLowerCase().includes('hello'));
|
||||
});
|
||||
|
||||
test('writes a file', (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
rig.createFile('test.txt', '');
|
||||
|
||||
rig.run(`edit test.txt to have a hello world message`);
|
||||
|
||||
const fileContent = rig.readFile('test.txt');
|
||||
assert.ok(fileContent.toLowerCase().includes('hello'));
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
test('should be able to search the web', async (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
|
||||
const prompt = `what planet do we live on`;
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
assert.ok(result.toLowerCase().includes('earth'));
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
test('should be able to list a directory', async (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
rig.createFile('file1.txt', 'file 1 content');
|
||||
rig.mkdir('subdir');
|
||||
|
||||
const prompt = `Can you list the files in the current directory`;
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
assert.ok(result.includes('file1.txt'));
|
||||
assert.ok(result.includes('subdir'));
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
test.skip('should be able to read multiple files', async (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
rig.createFile('file1.txt', 'file 1 content');
|
||||
rig.createFile('file2.txt', 'file 2 content');
|
||||
|
||||
const prompt = `Read the files in this directory, list them and print them to the screen`;
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
assert.ok(result.includes('file 1 content'));
|
||||
assert.ok(result.includes('file 2 content'));
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
test('should be able to replace content in a file', async (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
|
||||
const fileName = 'file_to_replace.txt';
|
||||
rig.createFile(fileName, 'original content');
|
||||
const prompt = `Can you replace 'original' with 'replaced' in the file 'file_to_replace.txt'`;
|
||||
|
||||
await rig.run(prompt);
|
||||
const newFileContent = rig.readFile(fileName);
|
||||
assert.strictEqual(newFileContent, 'replaced content');
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
import { spawn } from 'child_process';
|
||||
import { mkdirSync, rmSync, createWriteStream } from 'fs';
|
||||
import { join, dirname, basename } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { glob } from 'glob';
|
||||
|
||||
async function main() {
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
const integrationTestsDir = join(rootDir, '.integration-tests');
|
||||
|
||||
if (process.env.GEMINI_SANDBOX === 'docker' && !process.env.IS_DOCKER) {
|
||||
console.log('Building sandbox for Docker...');
|
||||
const buildResult = spawnSync('npm', ['run', 'build:all'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
if (buildResult.status !== 0) {
|
||||
console.error('Sandbox build failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const runId = `${Date.now()}`;
|
||||
const runDir = join(integrationTestsDir, runId);
|
||||
|
||||
mkdirSync(runDir, { recursive: true });
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const keepOutput =
|
||||
process.env.KEEP_OUTPUT === 'true' || args.includes('--keep-output');
|
||||
if (keepOutput) {
|
||||
const keepOutputIndex = args.indexOf('--keep-output');
|
||||
if (keepOutputIndex > -1) {
|
||||
args.splice(keepOutputIndex, 1);
|
||||
}
|
||||
console.log(`Keeping output for test run in: ${runDir}`);
|
||||
}
|
||||
|
||||
const verbose = args.includes('--verbose');
|
||||
if (verbose) {
|
||||
const verboseIndex = args.indexOf('--verbose');
|
||||
if (verboseIndex > -1) {
|
||||
args.splice(verboseIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const testPatterns =
|
||||
args.length > 0
|
||||
? args.map((arg) => `integration-tests/${arg}.test.js`)
|
||||
: ['integration-tests/*.test.js'];
|
||||
const testFiles = glob.sync(testPatterns, { cwd: rootDir, absolute: true });
|
||||
|
||||
for (const testFile of testFiles) {
|
||||
const testFileName = basename(testFile);
|
||||
console.log(`\tFound test file: ${testFileName}`);
|
||||
}
|
||||
|
||||
let allTestsPassed = true;
|
||||
|
||||
for (const testFile of testFiles) {
|
||||
const testFileName = basename(testFile);
|
||||
const testFileDir = join(runDir, testFileName);
|
||||
mkdirSync(testFileDir, { recursive: true });
|
||||
|
||||
console.log(
|
||||
`------------- Running test file: ${testFileName} ------------------------------`,
|
||||
);
|
||||
|
||||
const child = spawn('node', ['--test', testFile], {
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
INTEGRATION_TEST_FILE_DIR: testFileDir,
|
||||
KEEP_OUTPUT: keepOutput.toString(),
|
||||
TEST_FILE_NAME: testFileName,
|
||||
},
|
||||
});
|
||||
|
||||
if (verbose) {
|
||||
child.stdout.pipe(process.stdout);
|
||||
child.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
if (keepOutput) {
|
||||
const outputFile = join(testFileDir, 'output.log');
|
||||
const outputStream = createWriteStream(outputFile);
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
console.log(`Output for ${testFileName} written to: ${outputFile}`);
|
||||
} else if (!verbose) {
|
||||
child.stdout.pipe(process.stdout);
|
||||
child.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
const exitCode = await new Promise((resolve) => {
|
||||
child.on('close', resolve);
|
||||
});
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.error(`Test file failed: ${testFileName}`);
|
||||
allTestsPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!keepOutput) {
|
||||
rmSync(runDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (!allTestsPassed) {
|
||||
console.error('One or more test files failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
test('should be able to run a shell command', async (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
rig.createFile('blah.txt', 'some content');
|
||||
|
||||
const prompt = `Can you use ls to list the contexts of the current folder`;
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
assert.ok(result.includes('blah.txt'));
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
test('should be able to save to memory', async (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
|
||||
const prompt = `remember that my favorite color is blue.
|
||||
|
||||
what is my favorite color? tell me that and surround it with $ symbol`;
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
assert.ok(result.toLowerCase().includes('$blue$'));
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { spawn } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { writeFileSync, unlinkSync } from 'fs';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const serverScriptPath = join(__dirname, './temp-server.js');
|
||||
|
||||
const serverScript = `
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'addition-server',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
server.registerTool(
|
||||
'add',
|
||||
{
|
||||
title: 'Addition Tool',
|
||||
description: 'Add two numbers',
|
||||
inputSchema: { a: z.number(), b: z.number() },
|
||||
},
|
||||
async ({ a, b }) => ({
|
||||
content: [{ type: 'text', text: String(a + b) }],
|
||||
}),
|
||||
);
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
`;
|
||||
|
||||
describe('simple-mcp-server', () => {
|
||||
const rig = new TestRig();
|
||||
let child;
|
||||
|
||||
before(() => {
|
||||
writeFileSync(serverScriptPath, serverScript);
|
||||
child = spawn('node', [serverScriptPath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
child.stderr.on('data', (data) => {
|
||||
console.error(`stderr: ${data}`);
|
||||
});
|
||||
// Wait for the server to be ready
|
||||
return new Promise((resolve) => setTimeout(resolve, 500));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
child.kill();
|
||||
unlinkSync(serverScriptPath);
|
||||
});
|
||||
|
||||
test('should add two numbers', () => {
|
||||
rig.setup('should add two numbers');
|
||||
const output = rig.run('add 5 and 10');
|
||||
assert.ok(output.includes('15'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { env } from 'process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function sanitizeTestName(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '-')
|
||||
.replace(/-+/g, '-');
|
||||
}
|
||||
|
||||
export class TestRig {
|
||||
constructor() {
|
||||
this.bundlePath = join(__dirname, '..', 'bundle/gemini.js');
|
||||
this.testDir = null;
|
||||
}
|
||||
|
||||
setup(testName) {
|
||||
this.testName = testName;
|
||||
const sanitizedName = sanitizeTestName(testName);
|
||||
this.testDir = join(env.INTEGRATION_TEST_FILE_DIR, sanitizedName);
|
||||
mkdirSync(this.testDir, { recursive: true });
|
||||
}
|
||||
|
||||
createFile(fileName, content) {
|
||||
const filePath = join(this.testDir, fileName);
|
||||
writeFileSync(filePath, content);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
mkdir(dir) {
|
||||
mkdirSync(join(this.testDir, dir));
|
||||
}
|
||||
|
||||
run(prompt, ...args) {
|
||||
const output = execSync(
|
||||
`node ${this.bundlePath} --yolo --prompt "${prompt}" ${args.join(' ')}`,
|
||||
{
|
||||
cwd: this.testDir,
|
||||
encoding: 'utf-8',
|
||||
},
|
||||
);
|
||||
|
||||
if (env.KEEP_OUTPUT === 'true') {
|
||||
const testId = `${env.TEST_FILE_NAME.replace(
|
||||
'.test.js',
|
||||
'',
|
||||
)}:${this.testName.replace(/ /g, '-')}`;
|
||||
console.log(`--- TEST: ${testId} ---`);
|
||||
console.log(output);
|
||||
console.log(`--- END TEST: ${testId} ---`);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
readFile(fileName) {
|
||||
const content = readFileSync(join(this.testDir, fileName), 'utf-8');
|
||||
if (env.KEEP_OUTPUT === 'true') {
|
||||
const testId = `${env.TEST_FILE_NAME.replace(
|
||||
'.test.js',
|
||||
'',
|
||||
)}:${this.testName.replace(/ /g, '-')}`;
|
||||
console.log(`--- FILE: ${testId}/${fileName} ---`);
|
||||
console.log(content);
|
||||
console.log(`--- END FILE: ${testId}/${fileName} ---`);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'assert';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
test('should be able to write a file', async (t) => {
|
||||
const rig = new TestRig();
|
||||
rig.setup(t.name);
|
||||
const prompt = `show me an example of using the write tool. put a dad joke in dad.txt`;
|
||||
|
||||
await rig.run(prompt);
|
||||
const newFilePath = 'dad.txt';
|
||||
|
||||
const newFileContent = rig.readFile(newFilePath);
|
||||
assert.notEqual(newFileContent, '');
|
||||
});
|
||||
@@ -1,13 +1,76 @@
|
||||
{
|
||||
"name": "gemini-code",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"name": "gemini-cli",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build --workspaces",
|
||||
"generate": "node scripts/generate-git-commit-info.js",
|
||||
"build": "node scripts/build.js",
|
||||
"build:sandbox": "node scripts/build_sandbox.js",
|
||||
"build:all": "npm run build && npm run build:sandbox",
|
||||
"clean": "node scripts/clean.js",
|
||||
"prepare": "npm run bundle",
|
||||
"test": "npm run test --workspaces",
|
||||
"start": "npm run start --workspace=gemini-code-cli"
|
||||
"test:ci": "npm run test:ci --workspaces --if-present",
|
||||
"test:e2e": "npm run test:integration:sandbox:none -- --verbose --keep-output",
|
||||
"test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman",
|
||||
"test:integration:sandbox:none": "GEMINI_SANDBOX=false node integration-tests/run-tests.js",
|
||||
"test:integration:sandbox:docker": "GEMINI_SANDBOX=docker node integration-tests/run-tests.js",
|
||||
"test:integration:sandbox:podman": "GEMINI_SANDBOX=podman node integration-tests/run-tests.js",
|
||||
"start": "node scripts/start.js",
|
||||
"debug": "cross-env DEBUG=1 node scripts/start.js",
|
||||
"lint:fix": "eslint . --fix && eslint integration-tests --fix",
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
|
||||
"typecheck": "npm run typecheck --workspaces --if-present",
|
||||
"format": "prettier --write .",
|
||||
"preflight": "npm run clean && npm ci && npm run format && npm run lint:fix && npm run build && npm run typecheck && npm run test:ci",
|
||||
"auth:npm": "npx google-artifactregistry-auth",
|
||||
"auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev",
|
||||
"auth": "npm run auth:npm && npm run auth:docker",
|
||||
"prerelease:dev": "npm run prerelease:version --workspaces && npm run prerelease:deps --workspaces",
|
||||
"bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
|
||||
"build:cli": "npm run build --workspace packages/cli",
|
||||
"build:core": "npm run build --workspace packages/core",
|
||||
"build:packages": "npm run build:core && npm run build:cli",
|
||||
"build:docker": "node scripts/build_sandbox.js -s",
|
||||
"prepare:cli-packagejson": "node scripts/prepare-cli-packagejson.js",
|
||||
"publish:sandbox": "node scripts/publish-sandbox.js",
|
||||
"publish:npm": "npm publish --workspaces ${NPM_PUBLISH_TAG:+--tag=$NPM_PUBLISH_TAG} ${NPM_DRY_RUN:+--dry-run}",
|
||||
"publish:release": "npm run build:packages && npm run prepare:cli-packagejson && npm run build:docker && npm run publish:sandbox && npm run publish:npm",
|
||||
"telemetry": "node scripts/telemetry.js",
|
||||
"start:gcp": "npm run telemetry -- --target=gcp & npm start"
|
||||
},
|
||||
"bin": {
|
||||
"gemini": "bundle/gemini.js"
|
||||
},
|
||||
"files": [
|
||||
"bundle/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-license-header": "^0.8.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"glob": "^10.4.5",
|
||||
"globals": "^16.0.0",
|
||||
"json": "^11.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"memfs": "^4.17.2",
|
||||
"prettier": "^3.5.3",
|
||||
"react-devtools-core": "^4.28.5",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"yargs": "^17.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
# Gemini Code CLI
|
||||
# Gemini CLI
|
||||
|
||||
This package contains the core command-line interface for Gemini Code.
|
||||
This package contains the core command-line interface for Gemini CLI.
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Get a Gemini API Key:** Obtain your API key from Google AI Studio: [https://aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
|
||||
2. **Set Environment Variable:** Set the `GEMINI_API_KEY` environment variable to your obtained key. You can do this temporarily in your current shell session:
|
||||
```bash
|
||||
export GEMINI_API_KEY="YOUR_API_KEY"
|
||||
```
|
||||
Or add it to your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`) for persistence:
|
||||
```bash
|
||||
echo 'export GEMINI_API_KEY="YOUR_API_KEY"' >> ~/.bashrc # Or your preferred shell config file
|
||||
source ~/.bashrc # Reload the config
|
||||
```
|
||||
Replace `"YOUR_API_KEY"` with your actual key.
|
||||
|
||||
## Building
|
||||
|
||||
@@ -14,7 +28,7 @@ This command executes the TypeScript compiler (`tsc`) as defined in this package
|
||||
|
||||
## Running
|
||||
|
||||
To start the Gemini Code CLI directly from this directory:
|
||||
To start the Gemini CLI directly from this directory:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import './src/gemini.js';
|
||||
import { main } from './src/gemini.js';
|
||||
|
||||
// --- Global Entry Point ---
|
||||
main().catch((error) => {
|
||||
console.error('An unexpected critical error occurred:');
|
||||
if (error instanceof Error) {
|
||||
console.error(error.stack);
|
||||
} else {
|
||||
console.error(String(error));
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,38 +1,74 @@
|
||||
{
|
||||
"name": "gemini-code-cli",
|
||||
"version": "1.0.0",
|
||||
"description": "Gemini Code CLI",
|
||||
"type": "module",
|
||||
"main": "dist/gemini.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/gemini.js",
|
||||
"debug": "node --inspect-brk dist/gemini.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@google/genai": "^0.8.0",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"fast-glob": "^3.3.3",
|
||||
"ink": "^5.2.0",
|
||||
"ink-select-input": "^6.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"react": "^18.3.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
"name": "@gemini-cli/cli",
|
||||
"version": "0.1.0",
|
||||
"description": "Gemini CLI",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"gemini": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ../../scripts/build_package.js",
|
||||
"clean": "rm -rf dist",
|
||||
"start": "node dist/index.js",
|
||||
"debug": "node --inspect-brk dist/index.js",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
"test:ci": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prerelease:version": "node ../../scripts/bind_package_version.js",
|
||||
"prerelease:deps": "node ../../scripts/bind_package_dependencies.js",
|
||||
"prepublishOnly": "npm publish --workspace=@gemini-cli/core",
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "gemini-cli-sandbox"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gemini-cli/core": "file:../core",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"command-exists": "^1.2.9",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"glob": "^10.4.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "^5.2.0",
|
||||
"ink-big-text": "^2.0.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-select-input": "^6.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"lowlight": "^3.3.0",
|
||||
"mime-types": "^2.1.4",
|
||||
"open": "^10.1.2",
|
||||
"react": "^18.3.1",
|
||||
"read-package-up": "^11.0.0",
|
||||
"shell-quote": "^1.8.2",
|
||||
"string-width": "^7.1.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"update-notifier": "^7.3.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import yargs from 'yargs/yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
|
||||
export interface CliArgs {
|
||||
target_dir: string | undefined;
|
||||
_: (string | number)[]; // Captures positional arguments
|
||||
// Add other expected args here if needed
|
||||
// e.g., verbose?: boolean;
|
||||
}
|
||||
|
||||
export async function parseArguments(): Promise<CliArgs> {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.option('target_dir', {
|
||||
alias: 'd',
|
||||
type: 'string',
|
||||
description:
|
||||
'The target directory for Gemini operations. Defaults to the current working directory.',
|
||||
})
|
||||
.help()
|
||||
.alias('h', 'help')
|
||||
.strict() // Keep strict mode to error on unknown options
|
||||
.parseAsync();
|
||||
|
||||
// Handle warnings for extra arguments here
|
||||
if (argv._ && argv._.length > 0) {
|
||||
console.warn(
|
||||
`Warning: Additional arguments provided (${argv._.join(', ')}), but will be ignored.`
|
||||
);
|
||||
}
|
||||
|
||||
// Cast to the interface to ensure the structure aligns with expectations
|
||||
// Use `unknown` first for safer casting if types might not perfectly match
|
||||
return argv as unknown as CliArgs;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { AuthType } from '@gemini-cli/core';
|
||||
import { loadEnvironment } from './config.js';
|
||||
|
||||
export const validateAuthMethod = (authMethod: string): string | null => {
|
||||
loadEnvironment();
|
||||
if (authMethod === AuthType.LOGIN_WITH_GOOGLE_PERSONAL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authMethod === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE) {
|
||||
if (!process.env.GOOGLE_CLOUD_PROJECT) {
|
||||
return 'GOOGLE_CLOUD_PROJECT environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authMethod === AuthType.USE_GEMINI) {
|
||||
if (!process.env.GEMINI_API_KEY) {
|
||||
return 'GEMINI_API_KEY environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authMethod === AuthType.USE_VERTEX_AI) {
|
||||
if (!process.env.GOOGLE_API_KEY) {
|
||||
return 'GOOGLE_API_KEY environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
}
|
||||
if (!process.env.GOOGLE_CLOUD_PROJECT) {
|
||||
return 'GOOGLE_CLOUD_PROJECT environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
}
|
||||
if (!process.env.GOOGLE_CLOUD_LOCATION) {
|
||||
return 'GOOGLE_CLOUD_LOCATION environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'Invalid auth method selected.';
|
||||
};
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import {
|
||||
Config,
|
||||
ConfigParameters,
|
||||
ContentGeneratorConfig,
|
||||
} from '@gemini-cli/core';
|
||||
|
||||
const TEST_CONTENT_GENERATOR_CONFIG: ContentGeneratorConfig = {
|
||||
apiKey: 'test-key',
|
||||
model: 'test-model',
|
||||
userAgent: 'test-agent',
|
||||
};
|
||||
|
||||
// Mock file discovery service and tool registry
|
||||
vi.mock('@gemini-cli/core', async () => {
|
||||
const actual = await vi.importActual('@gemini-cli/core');
|
||||
return {
|
||||
...actual,
|
||||
FileDiscoveryService: vi.fn().mockImplementation(() => ({
|
||||
initialize: vi.fn(),
|
||||
})),
|
||||
createToolRegistry: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Configuration Integration Tests', () => {
|
||||
let tempDir: string;
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(tmpdir(), 'gemini-cli-test-'));
|
||||
originalEnv = { ...process.env };
|
||||
process.env.GEMINI_API_KEY = 'test-api-key';
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('File Filtering Configuration', () => {
|
||||
it('should load default file filtering settings', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: undefined, // Should default to true
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
|
||||
});
|
||||
|
||||
it('should load custom file filtering settings from configuration', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: false,
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(false);
|
||||
});
|
||||
|
||||
it('should merge user and workspace file filtering settings', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: true,
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration Integration', () => {
|
||||
it('should handle partial configuration objects gracefully', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: false,
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
// Specified settings should be applied
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty configuration objects gracefully', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: undefined,
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
// All settings should use defaults
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle missing configuration sections gracefully', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
// Missing fileFiltering configuration
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
// All git-aware settings should use defaults
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world Configuration Scenarios', () => {
|
||||
it('should handle a security-focused configuration', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: true,
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle a CI/CD environment configuration', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: false, // CI might need to see all files
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checkpointing Configuration', () => {
|
||||
it('should enable checkpointing when the setting is true', async () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
checkpointing: true,
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
|
||||
expect(config.getCheckpointingEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as os from 'os';
|
||||
import { loadCliConfig } from './config.js';
|
||||
import { Settings } from './settings.js';
|
||||
import { Extension } from './extension.js';
|
||||
import * as ServerConfig from '@gemini-cli/core';
|
||||
|
||||
const MOCK_HOME_DIR = '/mock/home/user';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const actualOs = await importOriginal<typeof os>();
|
||||
return {
|
||||
...actualOs,
|
||||
homedir: vi.fn(() => MOCK_HOME_DIR),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('open', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('read-package-up', () => ({
|
||||
readPackageUp: vi.fn(() =>
|
||||
Promise.resolve({ packageJson: { version: 'test-version' } }),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@gemini-cli/core', async () => {
|
||||
const actualServer =
|
||||
await vi.importActual<typeof ServerConfig>('@gemini-cli/core');
|
||||
return {
|
||||
...actualServer,
|
||||
loadEnvironment: vi.fn(),
|
||||
loadServerHierarchicalMemory: vi.fn(
|
||||
(cwd, debug, fileService, extensionPaths) =>
|
||||
Promise.resolve({
|
||||
memoryContent: extensionPaths?.join(',') || '',
|
||||
fileCount: extensionPaths?.length || 0,
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('loadCliConfig', () => {
|
||||
const originalArgv = process.argv;
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue(MOCK_HOME_DIR);
|
||||
process.env.GEMINI_API_KEY = 'test-api-key'; // Ensure API key is set for tests
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
process.env = originalEnv;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should set showMemoryUsage to true when --memory flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--show_memory_usage'];
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getShowMemoryUsage()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set showMemoryUsage to false when --memory flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getShowMemoryUsage()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = { showMemoryUsage: false };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getShowMemoryUsage()).toBe(false);
|
||||
});
|
||||
|
||||
it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => {
|
||||
process.argv = ['node', 'script.js', '--show_memory_usage'];
|
||||
const settings: Settings = { showMemoryUsage: false };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getShowMemoryUsage()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig telemetry', () => {
|
||||
const originalArgv = process.argv;
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue(MOCK_HOME_DIR);
|
||||
process.env.GEMINI_API_KEY = 'test-api-key';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
process.env = originalEnv;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should set telemetry to false by default when no flag or setting is present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set telemetry to true when --telemetry flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry'];
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set telemetry to false when --no-telemetry flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--no-telemetry'];
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = { telemetry: { enabled: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry'];
|
||||
const settings: Settings = { telemetry: { enabled: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => {
|
||||
process.argv = ['node', 'script.js', '--no-telemetry'];
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should use telemetry OTLP endpoint from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = {
|
||||
telemetry: { otlpEndpoint: 'http://settings.example.com' },
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryOtlpEndpoint()).toBe(
|
||||
'http://settings.example.com',
|
||||
);
|
||||
});
|
||||
|
||||
it('should prioritize --telemetry-otlp-endpoint CLI flag over settings', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--telemetry-otlp-endpoint',
|
||||
'http://cli.example.com',
|
||||
];
|
||||
const settings: Settings = {
|
||||
telemetry: { otlpEndpoint: 'http://settings.example.com' },
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryOtlpEndpoint()).toBe('http://cli.example.com');
|
||||
});
|
||||
|
||||
it('should use default endpoint if no OTLP endpoint is provided via CLI or settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317');
|
||||
});
|
||||
|
||||
it('should use telemetry target from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = {
|
||||
telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryTarget()).toBe(
|
||||
ServerConfig.DEFAULT_TELEMETRY_TARGET,
|
||||
);
|
||||
});
|
||||
|
||||
it('should prioritize --telemetry-target CLI flag over settings', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry-target', 'gcp'];
|
||||
const settings: Settings = {
|
||||
telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET },
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryTarget()).toBe('gcp');
|
||||
});
|
||||
|
||||
it('should use default target if no target is provided via CLI or settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryTarget()).toBe(
|
||||
ServerConfig.DEFAULT_TELEMETRY_TARGET,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use telemetry log prompts from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = { telemetry: { logPrompts: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should prioritize --telemetry-log-prompts CLI flag (true) over settings (false)', async () => {
|
||||
process.argv = ['node', 'script.js', '--telemetry-log-prompts'];
|
||||
const settings: Settings = { telemetry: { logPrompts: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize --no-telemetry-log-prompts CLI flag (false) over settings (true)', async () => {
|
||||
process.argv = ['node', 'script.js', '--no-telemetry-log-prompts'];
|
||||
const settings: Settings = { telemetry: { logPrompts: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should use default log prompts (true) if no value is provided via CLI or settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = { telemetry: { enabled: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session');
|
||||
expect(config.getTelemetryLogPromptsEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue(MOCK_HOME_DIR);
|
||||
// Other common mocks would be reset here.
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = {};
|
||||
const extensions: Extension[] = [
|
||||
{
|
||||
config: {
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
},
|
||||
contextFiles: ['/path/to/ext1/GEMINI.md'],
|
||||
},
|
||||
{
|
||||
config: {
|
||||
name: 'ext2',
|
||||
version: '1.0.0',
|
||||
},
|
||||
contextFiles: [],
|
||||
},
|
||||
{
|
||||
config: {
|
||||
name: 'ext3',
|
||||
version: '1.0.0',
|
||||
},
|
||||
contextFiles: [
|
||||
'/path/to/ext3/context1.md',
|
||||
'/path/to/ext3/context2.md',
|
||||
],
|
||||
},
|
||||
];
|
||||
await loadCliConfig(settings, extensions, 'session-id');
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
false,
|
||||
expect.any(Object),
|
||||
[
|
||||
'/path/to/ext1/GEMINI.md',
|
||||
'/path/to/ext3/context1.md',
|
||||
'/path/to/ext3/context2.md',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// NOTE TO FUTURE DEVELOPERS:
|
||||
// To re-enable tests for loadHierarchicalGeminiMemory, ensure that:
|
||||
// 1. os.homedir() is reliably mocked *before* the config.ts module is loaded
|
||||
// and its functions (which use os.homedir()) are called.
|
||||
// 2. fs/promises and fs mocks correctly simulate file/directory existence,
|
||||
// readability, and content based on paths derived from the mocked os.homedir().
|
||||
// 3. Spies on console functions (for logger output) are correctly set up if needed.
|
||||
// Example of a previously failing test structure:
|
||||
/*
|
||||
it('should correctly use mocked homedir for global path', async () => {
|
||||
const MOCK_GEMINI_DIR_LOCAL = path.join(MOCK_HOME_DIR, '.gemini');
|
||||
const MOCK_GLOBAL_PATH_LOCAL = path.join(MOCK_GEMINI_DIR_LOCAL, 'GEMINI.md');
|
||||
mockFs({
|
||||
[MOCK_GLOBAL_PATH_LOCAL]: { type: 'file', content: 'GlobalContentOnly' }
|
||||
});
|
||||
const memory = await loadHierarchicalGeminiMemory("/some/other/cwd", false);
|
||||
expect(memory).toBe('GlobalContentOnly');
|
||||
expect(vi.mocked(os.homedir)).toHaveBeenCalled();
|
||||
expect(fsPromises.readFile).toHaveBeenCalledWith(MOCK_GLOBAL_PATH_LOCAL, 'utf-8');
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
describe('mergeMcpServers', () => {
|
||||
it('should not modify the original settings object', async () => {
|
||||
const settings: Settings = {
|
||||
mcpServers: {
|
||||
'test-server': {
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
},
|
||||
};
|
||||
const extensions: Extension[] = [
|
||||
{
|
||||
config: {
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
mcpServers: {
|
||||
'ext1-server': {
|
||||
url: 'http://localhost:8081',
|
||||
},
|
||||
},
|
||||
},
|
||||
contextFiles: [],
|
||||
},
|
||||
];
|
||||
const originalSettings = JSON.parse(JSON.stringify(settings));
|
||||
await loadCliConfig(settings, extensions, 'test-session');
|
||||
expect(settings).toEqual(originalSettings);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import yargs from 'yargs/yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import {
|
||||
Config,
|
||||
loadServerHierarchicalMemory,
|
||||
setGeminiMdFilename as setServerGeminiMdFilename,
|
||||
getCurrentGeminiMdFilename,
|
||||
ApprovalMode,
|
||||
GEMINI_CONFIG_DIR as GEMINI_DIR,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
FileDiscoveryService,
|
||||
TelemetryTarget,
|
||||
} from '@gemini-cli/core';
|
||||
import { Settings } from './settings.js';
|
||||
|
||||
import { Extension } from './extension.js';
|
||||
import { getCliVersion } from '../utils/version.js';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
|
||||
// Simple console logger for now - replace with actual logger if available
|
||||
const logger = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
debug: (...args: any[]) => console.debug('[DEBUG]', ...args),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
warn: (...args: any[]) => console.warn('[WARN]', ...args),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: (...args: any[]) => console.error('[ERROR]', ...args),
|
||||
};
|
||||
|
||||
interface CliArgs {
|
||||
model: string | undefined;
|
||||
sandbox: boolean | string | undefined;
|
||||
'sandbox-image': string | undefined;
|
||||
debug: boolean | undefined;
|
||||
prompt: string | undefined;
|
||||
all_files: boolean | undefined;
|
||||
show_memory_usage: boolean | undefined;
|
||||
yolo: boolean | undefined;
|
||||
telemetry: boolean | undefined;
|
||||
checkpointing: boolean | undefined;
|
||||
telemetryTarget: string | undefined;
|
||||
telemetryOtlpEndpoint: string | undefined;
|
||||
telemetryLogPrompts: boolean | undefined;
|
||||
}
|
||||
|
||||
async function parseArguments(): Promise<CliArgs> {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.option('model', {
|
||||
alias: 'm',
|
||||
type: 'string',
|
||||
description: `Model`,
|
||||
default: process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
|
||||
})
|
||||
.option('prompt', {
|
||||
alias: 'p',
|
||||
type: 'string',
|
||||
description: 'Prompt. Appended to input on stdin (if any).',
|
||||
})
|
||||
.option('sandbox', {
|
||||
alias: 's',
|
||||
type: 'boolean',
|
||||
description: 'Run in sandbox?',
|
||||
})
|
||||
.option('sandbox-image', {
|
||||
type: 'string',
|
||||
description: 'Sandbox image URI.',
|
||||
})
|
||||
.option('debug', {
|
||||
alias: 'd',
|
||||
type: 'boolean',
|
||||
description: 'Run in debug mode?',
|
||||
default: false,
|
||||
})
|
||||
.option('all_files', {
|
||||
alias: 'a',
|
||||
type: 'boolean',
|
||||
description: 'Include ALL files in context?',
|
||||
default: false,
|
||||
})
|
||||
.option('show_memory_usage', {
|
||||
type: 'boolean',
|
||||
description: 'Show memory usage in status bar',
|
||||
default: false,
|
||||
})
|
||||
.option('yolo', {
|
||||
alias: 'y',
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Automatically accept all actions (aka YOLO mode, see https://www.youtube.com/watch?v=xvFZjo5PgG0 for more details)?',
|
||||
default: false,
|
||||
})
|
||||
.option('telemetry', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Enable telemetry? This flag specifically controls if telemetry is sent. Other --telemetry-* flags set specific values but do not enable telemetry on their own.',
|
||||
})
|
||||
.option('telemetry-target', {
|
||||
type: 'string',
|
||||
choices: ['local', 'gcp'],
|
||||
description:
|
||||
'Set the telemetry target (local or gcp). Overrides settings files.',
|
||||
})
|
||||
.option('telemetry-otlp-endpoint', {
|
||||
type: 'string',
|
||||
description:
|
||||
'Set the OTLP endpoint for telemetry. Overrides environment variables and settings files.',
|
||||
})
|
||||
.option('telemetry-log-prompts', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Enable or disable logging of user prompts for telemetry. Overrides settings files.',
|
||||
})
|
||||
.option('checkpointing', {
|
||||
alias: 'c',
|
||||
type: 'boolean',
|
||||
description: 'Enables checkpointing of file edits',
|
||||
default: false,
|
||||
})
|
||||
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
||||
.alias('v', 'version')
|
||||
.help()
|
||||
.alias('h', 'help')
|
||||
.strict().argv;
|
||||
|
||||
return argv;
|
||||
}
|
||||
|
||||
// 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,
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
): Promise<{ memoryContent: string; fileCount: number }> {
|
||||
if (debugMode) {
|
||||
logger.debug(
|
||||
`CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory}`,
|
||||
);
|
||||
}
|
||||
// Directly call the server function.
|
||||
// The server function will use its own homedir() for the global path.
|
||||
return loadServerHierarchicalMemory(
|
||||
currentWorkingDirectory,
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadCliConfig(
|
||||
settings: Settings,
|
||||
extensions: Extension[],
|
||||
sessionId: string,
|
||||
): Promise<Config> {
|
||||
loadEnvironment();
|
||||
|
||||
const argv = await parseArguments();
|
||||
const debugMode = argv.debug || false;
|
||||
|
||||
// Set the context filename in the server's memoryTool module BEFORE loading memory
|
||||
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
|
||||
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
|
||||
// However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
|
||||
if (settings.contextFileName) {
|
||||
setServerGeminiMdFilename(settings.contextFileName);
|
||||
} else {
|
||||
// Reset to default if not provided in settings.
|
||||
setServerGeminiMdFilename(getCurrentGeminiMdFilename());
|
||||
}
|
||||
|
||||
const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles);
|
||||
|
||||
const fileService = new FileDiscoveryService(process.cwd());
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
|
||||
process.cwd(),
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
);
|
||||
|
||||
const mcpServers = mergeMcpServers(settings, extensions);
|
||||
|
||||
const sandboxConfig = await loadSandboxConfig(settings, argv);
|
||||
|
||||
return new Config({
|
||||
sessionId,
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
sandbox: sandboxConfig,
|
||||
targetDir: process.cwd(),
|
||||
debugMode,
|
||||
question: argv.prompt || '',
|
||||
fullContext: argv.all_files || false,
|
||||
coreTools: settings.coreTools || undefined,
|
||||
excludeTools: settings.excludeTools || undefined,
|
||||
toolDiscoveryCommand: settings.toolDiscoveryCommand,
|
||||
toolCallCommand: settings.toolCallCommand,
|
||||
mcpServerCommand: settings.mcpServerCommand,
|
||||
mcpServers,
|
||||
userMemory: memoryContent,
|
||||
geminiMdFileCount: fileCount,
|
||||
approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT,
|
||||
showMemoryUsage:
|
||||
argv.show_memory_usage || settings.showMemoryUsage || false,
|
||||
accessibility: settings.accessibility,
|
||||
telemetry: {
|
||||
enabled: argv.telemetry ?? settings.telemetry?.enabled,
|
||||
target: (argv.telemetryTarget ??
|
||||
settings.telemetry?.target) as TelemetryTarget,
|
||||
otlpEndpoint:
|
||||
argv.telemetryOtlpEndpoint ??
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
|
||||
settings.telemetry?.otlpEndpoint,
|
||||
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
|
||||
},
|
||||
// Git-aware file filtering settings
|
||||
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore,
|
||||
checkpointing: argv.checkpointing || settings.checkpointing?.enabled,
|
||||
proxy:
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy ||
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy,
|
||||
cwd: process.cwd(),
|
||||
fileDiscoveryService: fileService,
|
||||
bugCommand: settings.bugCommand,
|
||||
model: argv.model!,
|
||||
});
|
||||
}
|
||||
|
||||
function mergeMcpServers(settings: Settings, extensions: Extension[]) {
|
||||
const mcpServers = { ...(settings.mcpServers || {}) };
|
||||
for (const extension of extensions) {
|
||||
Object.entries(extension.config.mcpServers || {}).forEach(
|
||||
([key, server]) => {
|
||||
if (mcpServers[key]) {
|
||||
logger.warn(
|
||||
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
mcpServers[key] = server;
|
||||
},
|
||||
);
|
||||
}
|
||||
return mcpServers;
|
||||
}
|
||||
function findEnvFile(startDir: string): string | null {
|
||||
let currentDir = path.resolve(startDir);
|
||||
while (true) {
|
||||
// prefer gemini-specific .env under GEMINI_DIR
|
||||
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
|
||||
if (fs.existsSync(geminiEnvPath)) {
|
||||
return geminiEnvPath;
|
||||
}
|
||||
const envPath = path.join(currentDir, '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
return envPath;
|
||||
}
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir || !parentDir) {
|
||||
// check .env under home as fallback, again preferring gemini-specific .env
|
||||
const homeGeminiEnvPath = path.join(os.homedir(), GEMINI_DIR, '.env');
|
||||
if (fs.existsSync(homeGeminiEnvPath)) {
|
||||
return homeGeminiEnvPath;
|
||||
}
|
||||
const homeEnvPath = path.join(os.homedir(), '.env');
|
||||
if (fs.existsSync(homeEnvPath)) {
|
||||
return homeEnvPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadEnvironment(): void {
|
||||
const envFilePath = findEnvFile(process.cwd());
|
||||
if (envFilePath) {
|
||||
dotenv.config({ path: envFilePath });
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
function findEnvFile(startDir: string): string | null {
|
||||
// Start search from the provided directory (e.g., current working directory)
|
||||
let currentDir = path.resolve(startDir); // Ensure absolute path
|
||||
while (true) {
|
||||
const envPath = path.join(currentDir, '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
return envPath;
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir || !parentDir) {
|
||||
return null;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadEnvironment(): void {
|
||||
// Start searching from the current working directory by default
|
||||
const envFilePath = findEnvFile(process.cwd());
|
||||
|
||||
if (!envFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
dotenv.config({ path: envFilePath });
|
||||
|
||||
if (!process.env.GEMINI_API_KEY) {
|
||||
console.error('Error: GEMINI_API_KEY environment variable is not set in the loaded .env file.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function getApiKey(): string {
|
||||
loadEnvironment();
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('GEMINI_API_KEY is missing. Ensure loadEnvironment() was called successfully.');
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
EXTENSIONS_DIRECTORY_NAME,
|
||||
loadExtensions,
|
||||
} from './extension.js';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const os = await importOriginal<typeof import('os')>();
|
||||
return {
|
||||
...os,
|
||||
homedir: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('loadExtensions', () => {
|
||||
let tempWorkspaceDir: string;
|
||||
let tempHomeDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempWorkspaceDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-test-workspace-'),
|
||||
);
|
||||
tempHomeDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||
);
|
||||
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempWorkspaceDir, { recursive: true, force: true });
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should load context file path when GEMINI.md is present', () => {
|
||||
const workspaceExtensionsDir = path.join(
|
||||
tempWorkspaceDir,
|
||||
EXTENSIONS_DIRECTORY_NAME,
|
||||
);
|
||||
fs.mkdirSync(workspaceExtensionsDir, { recursive: true });
|
||||
createExtension(workspaceExtensionsDir, 'ext1', '1.0.0', true);
|
||||
createExtension(workspaceExtensionsDir, 'ext2', '2.0.0');
|
||||
|
||||
const extensions = loadExtensions(tempWorkspaceDir);
|
||||
|
||||
expect(extensions).toHaveLength(2);
|
||||
const ext1 = extensions.find((e) => e.config.name === 'ext1');
|
||||
const ext2 = extensions.find((e) => e.config.name === 'ext2');
|
||||
expect(ext1?.contextFiles).toEqual([
|
||||
path.join(workspaceExtensionsDir, 'ext1', 'GEMINI.md'),
|
||||
]);
|
||||
expect(ext2?.contextFiles).toEqual([]);
|
||||
});
|
||||
|
||||
it('should load context file path from the extension config', () => {
|
||||
const workspaceExtensionsDir = path.join(
|
||||
tempWorkspaceDir,
|
||||
EXTENSIONS_DIRECTORY_NAME,
|
||||
);
|
||||
fs.mkdirSync(workspaceExtensionsDir, { recursive: true });
|
||||
createExtension(
|
||||
workspaceExtensionsDir,
|
||||
'ext1',
|
||||
'1.0.0',
|
||||
false,
|
||||
'my-context-file.md',
|
||||
);
|
||||
|
||||
const extensions = loadExtensions(tempWorkspaceDir);
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
const ext1 = extensions.find((e) => e.config.name === 'ext1');
|
||||
expect(ext1?.contextFiles).toEqual([
|
||||
path.join(workspaceExtensionsDir, 'ext1', 'my-context-file.md'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function createExtension(
|
||||
extensionsDir: string,
|
||||
name: string,
|
||||
version: string,
|
||||
addContextFile = false,
|
||||
contextFileName?: string,
|
||||
): void {
|
||||
const extDir = path.join(extensionsDir, name);
|
||||
fs.mkdirSync(extDir);
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
|
||||
JSON.stringify({ name, version, contextFileName }),
|
||||
);
|
||||
|
||||
if (addContextFile) {
|
||||
fs.writeFileSync(path.join(extDir, 'GEMINI.md'), 'context');
|
||||
}
|
||||
|
||||
if (contextFileName) {
|
||||
fs.writeFileSync(path.join(extDir, contextFileName), 'context');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { MCPServerConfig } from '@gemini-cli/core';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
|
||||
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
|
||||
|
||||
export interface Extension {
|
||||
config: ExtensionConfig;
|
||||
contextFiles: string[];
|
||||
}
|
||||
|
||||
export interface ExtensionConfig {
|
||||
name: string;
|
||||
version: string;
|
||||
mcpServers?: Record<string, MCPServerConfig>;
|
||||
contextFileName?: string | string[];
|
||||
}
|
||||
|
||||
export function loadExtensions(workspaceDir: string): Extension[] {
|
||||
const allExtensions = [
|
||||
...loadExtensionsFromDir(workspaceDir),
|
||||
...loadExtensionsFromDir(os.homedir()),
|
||||
];
|
||||
|
||||
const uniqueExtensions: Extension[] = [];
|
||||
const seenNames = new Set<string>();
|
||||
for (const extension of allExtensions) {
|
||||
if (!seenNames.has(extension.config.name)) {
|
||||
console.log(
|
||||
`Loading extension: ${extension.config.name} (version: ${extension.config.version})`,
|
||||
);
|
||||
uniqueExtensions.push(extension);
|
||||
seenNames.add(extension.config.name);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueExtensions;
|
||||
}
|
||||
|
||||
function loadExtensionsFromDir(dir: string): Extension[] {
|
||||
const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME);
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const extensions: Extension[] = [];
|
||||
for (const subdir of fs.readdirSync(extensionsDir)) {
|
||||
const extensionDir = path.join(extensionsDir, subdir);
|
||||
|
||||
const extension = loadExtension(extensionDir);
|
||||
if (extension != null) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
function loadExtension(extensionDir: string): Extension | null {
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
console.error(
|
||||
`Warning: unexpected file ${extensionDir} in extensions directory.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
if (!fs.existsSync(configFilePath)) {
|
||||
console.error(
|
||||
`Warning: extension directory ${extensionDir} does not contain a config file ${configFilePath}.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const configContent = fs.readFileSync(configFilePath, 'utf-8');
|
||||
const config = JSON.parse(configContent) as ExtensionConfig;
|
||||
if (!config.name || !config.version) {
|
||||
console.error(
|
||||
`Invalid extension config in ${configFilePath}: missing name or version.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const contextFiles = getContextFileNames(config)
|
||||
.map((contextFileName) => path.join(extensionDir, contextFileName))
|
||||
.filter((contextFilePath) => fs.existsSync(contextFilePath));
|
||||
|
||||
return {
|
||||
config,
|
||||
contextFiles,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Warning: error parsing extension config in ${configFilePath}: ${e}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getContextFileNames(config: ExtensionConfig): string[] {
|
||||
if (!config.contextFileName) {
|
||||
return ['GEMINI.md'];
|
||||
} else if (!Array.isArray(config.contextFileName)) {
|
||||
return [config.contextFileName];
|
||||
}
|
||||
return config.contextFileName;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { SandboxConfig } from '@gemini-cli/core';
|
||||
import commandExists from 'command-exists';
|
||||
import * as os from 'node:os';
|
||||
import { getPackageJson } from '../utils/package.js';
|
||||
import { Settings } from './settings.js';
|
||||
|
||||
// This is a stripped-down version of the CliArgs interface from config.ts
|
||||
// to avoid circular dependencies.
|
||||
interface SandboxCliArgs {
|
||||
sandbox?: boolean | string;
|
||||
'sandbox-image'?: string;
|
||||
}
|
||||
|
||||
const VALID_SANDBOX_COMMANDS: ReadonlyArray<SandboxConfig['command']> = [
|
||||
'docker',
|
||||
'podman',
|
||||
'sandbox-exec',
|
||||
];
|
||||
|
||||
function isSandboxCommand(value: string): value is SandboxConfig['command'] {
|
||||
return (VALID_SANDBOX_COMMANDS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
function getSandboxCommand(
|
||||
sandbox?: boolean | string,
|
||||
): SandboxConfig['command'] | '' {
|
||||
// If the SANDBOX env var is set, we're already inside the sandbox.
|
||||
if (process.env.SANDBOX) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// note environment variable takes precedence over argument (from command line or settings)
|
||||
sandbox = process.env.GEMINI_SANDBOX?.toLowerCase().trim() ?? sandbox;
|
||||
if (sandbox === '1' || sandbox === 'true') sandbox = true;
|
||||
else if (sandbox === '0' || sandbox === 'false' || !sandbox) sandbox = false;
|
||||
|
||||
if (sandbox === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof sandbox === 'string' && sandbox) {
|
||||
if (!isSandboxCommand(sandbox)) {
|
||||
console.error(
|
||||
`ERROR: invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// confirm that specfied command exists
|
||||
if (commandExists.sync(sandbox)) {
|
||||
return sandbox;
|
||||
}
|
||||
console.error(
|
||||
`ERROR: missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// look for seatbelt, docker, or podman, in that order
|
||||
// for container-based sandboxing, require sandbox to be enabled explicitly
|
||||
if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) {
|
||||
return 'sandbox-exec';
|
||||
} else if (commandExists.sync('docker') && sandbox === true) {
|
||||
return 'docker';
|
||||
} else if (commandExists.sync('podman') && sandbox === true) {
|
||||
return 'podman';
|
||||
}
|
||||
|
||||
// throw an error if user requested sandbox but no command was found
|
||||
if (sandbox === true) {
|
||||
console.error(
|
||||
'ERROR: GEMINI_SANDBOX is true but failed to determine command for sandbox; ' +
|
||||
'install docker or podman or specify command in GEMINI_SANDBOX',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function loadSandboxConfig(
|
||||
settings: Settings,
|
||||
argv: SandboxCliArgs,
|
||||
): Promise<SandboxConfig | undefined> {
|
||||
const sandboxOption = argv.sandbox ?? settings.sandbox;
|
||||
const command = getSandboxCommand(sandboxOption);
|
||||
|
||||
const packageJson = await getPackageJson();
|
||||
const image =
|
||||
argv['sandbox-image'] ??
|
||||
process.env.GEMINI_SANDBOX_IMAGE ??
|
||||
packageJson?.config?.sandboxImageUri;
|
||||
|
||||
return command && image ? { command, image } : undefined;
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/// <reference types="vitest/globals" />
|
||||
|
||||
const MOCK_HOME_DIR = '/mock/home/user'; // MUST BE FIRST
|
||||
|
||||
// Mock 'os' first. Its factory uses MOCK_HOME_DIR.
|
||||
import * as osActual from 'os'; // Import for type info for the mock factory
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const actualOs = await importOriginal<typeof osActual>();
|
||||
return {
|
||||
...actualOs,
|
||||
homedir: vi.fn(() => MOCK_HOME_DIR),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock './settings.js' to ensure it uses the mocked 'os.homedir()' for its internal constants.
|
||||
vi.mock('./settings.js', async (importActual) => {
|
||||
const originalModule = await importActual<typeof import('./settings.js')>();
|
||||
return {
|
||||
__esModule: true, // Ensure correct module shape
|
||||
...originalModule, // Re-export all original members
|
||||
// We are relying on originalModule's USER_SETTINGS_PATH being constructed with mocked os.homedir()
|
||||
};
|
||||
});
|
||||
|
||||
// NOW import everything else, including the (now effectively re-exported) settings.js
|
||||
import * as pathActual from 'path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mocked,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import * as fs from 'fs'; // fs will be mocked separately
|
||||
import stripJsonComments from 'strip-json-comments'; // Will be mocked separately
|
||||
|
||||
// These imports will get the versions from the vi.mock('./settings.js', ...) factory.
|
||||
import {
|
||||
LoadedSettings,
|
||||
loadSettings,
|
||||
USER_SETTINGS_PATH, // This IS the mocked path.
|
||||
SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock.
|
||||
SettingScope,
|
||||
} from './settings.js';
|
||||
|
||||
const MOCK_WORKSPACE_DIR = '/mock/workspace';
|
||||
// Use the (mocked) SETTINGS_DIRECTORY_NAME for consistency
|
||||
const MOCK_WORKSPACE_SETTINGS_PATH = pathActual.join(
|
||||
MOCK_WORKSPACE_DIR,
|
||||
SETTINGS_DIRECTORY_NAME,
|
||||
'settings.json',
|
||||
);
|
||||
|
||||
vi.mock('fs');
|
||||
vi.mock('strip-json-comments', () => ({
|
||||
default: vi.fn((content) => content),
|
||||
}));
|
||||
|
||||
describe('Settings Loading and Merging', () => {
|
||||
let mockFsExistsSync: Mocked<typeof fs.existsSync>;
|
||||
let mockStripJsonComments: Mocked<typeof stripJsonComments>;
|
||||
let mockFsMkdirSync: Mocked<typeof fs.mkdirSync>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
mockFsExistsSync = vi.mocked(fs.existsSync);
|
||||
mockFsMkdirSync = vi.mocked(fs.mkdirSync);
|
||||
mockStripJsonComments = vi.mocked(stripJsonComments);
|
||||
|
||||
vi.mocked(osActual.homedir).mockReturnValue(MOCK_HOME_DIR);
|
||||
(mockStripJsonComments as unknown as Mock).mockImplementation(
|
||||
(jsonString: string) => jsonString,
|
||||
);
|
||||
(mockFsExistsSync as Mock).mockReturnValue(false);
|
||||
(fs.readFileSync as Mock).mockReturnValue('{}'); // Return valid empty JSON
|
||||
(mockFsMkdirSync as Mock).mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('loadSettings', () => {
|
||||
it('should load empty settings if no files exist', () => {
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.user.settings).toEqual({});
|
||||
expect(settings.workspace.settings).toEqual({});
|
||||
expect(settings.merged).toEqual({});
|
||||
expect(settings.errors.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should load user settings if only user file exists', () => {
|
||||
const expectedUserSettingsPath = USER_SETTINGS_PATH; // Use the path actually resolved by the (mocked) module
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === expectedUserSettingsPath,
|
||||
);
|
||||
const userSettingsContent = {
|
||||
theme: 'dark',
|
||||
contextFileName: 'USER_CONTEXT.md',
|
||||
};
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === expectedUserSettingsPath)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(
|
||||
expectedUserSettingsPath,
|
||||
'utf-8',
|
||||
);
|
||||
expect(settings.user.settings).toEqual(userSettingsContent);
|
||||
expect(settings.workspace.settings).toEqual({});
|
||||
expect(settings.merged).toEqual(userSettingsContent);
|
||||
});
|
||||
|
||||
it('should load workspace settings if only workspace file exists', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
);
|
||||
const workspaceSettingsContent = {
|
||||
sandbox: true,
|
||||
contextFileName: 'WORKSPACE_CONTEXT.md',
|
||||
};
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(
|
||||
MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
'utf-8',
|
||||
);
|
||||
expect(settings.user.settings).toEqual({});
|
||||
expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
|
||||
expect(settings.merged).toEqual(workspaceSettingsContent);
|
||||
});
|
||||
|
||||
it('should merge user and workspace settings, with workspace taking precedence', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const userSettingsContent = {
|
||||
theme: 'dark',
|
||||
sandbox: false,
|
||||
contextFileName: 'USER_CONTEXT.md',
|
||||
};
|
||||
const workspaceSettingsContent = {
|
||||
sandbox: true,
|
||||
coreTools: ['tool1'],
|
||||
contextFileName: 'WORKSPACE_CONTEXT.md',
|
||||
};
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.user.settings).toEqual(userSettingsContent);
|
||||
expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
|
||||
expect(settings.merged).toEqual({
|
||||
theme: 'dark',
|
||||
sandbox: true,
|
||||
coreTools: ['tool1'],
|
||||
contextFileName: 'WORKSPACE_CONTEXT.md',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle contextFileName correctly when only in user settings', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
const userSettingsContent = { contextFileName: 'CUSTOM.md' };
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.contextFileName).toBe('CUSTOM.md');
|
||||
});
|
||||
|
||||
it('should handle contextFileName correctly when only in workspace settings', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
);
|
||||
const workspaceSettingsContent = {
|
||||
contextFileName: 'PROJECT_SPECIFIC.md',
|
||||
};
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.contextFileName).toBe('PROJECT_SPECIFIC.md');
|
||||
});
|
||||
|
||||
it('should default contextFileName to undefined if not in any settings file', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const userSettingsContent = { theme: 'dark' };
|
||||
const workspaceSettingsContent = { sandbox: true };
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.contextFileName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should load telemetry setting from user settings', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
const userSettingsContent = { telemetry: true };
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.telemetry).toBe(true);
|
||||
});
|
||||
|
||||
it('should load telemetry setting from workspace settings', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
);
|
||||
const workspaceSettingsContent = { telemetry: false };
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.telemetry).toBe(false);
|
||||
});
|
||||
|
||||
it('should prioritize workspace telemetry setting over user setting', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const userSettingsContent = { telemetry: true };
|
||||
const workspaceSettingsContent = { telemetry: false };
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.telemetry).toBe(false);
|
||||
});
|
||||
|
||||
it('should have telemetry as undefined if not in any settings file', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist
|
||||
(fs.readFileSync as Mock).mockReturnValue('{}');
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.telemetry).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle JSON parsing errors gracefully', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true); // Both files "exist"
|
||||
const invalidJsonContent = 'invalid json';
|
||||
const userReadError = new SyntaxError(
|
||||
"Expected ',' or '}' after property value in JSON at position 10",
|
||||
);
|
||||
const workspaceReadError = new SyntaxError(
|
||||
'Unexpected token i in JSON at position 0',
|
||||
);
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH) {
|
||||
// Simulate JSON.parse throwing for user settings
|
||||
vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
|
||||
throw userReadError;
|
||||
});
|
||||
return invalidJsonContent; // Content that would cause JSON.parse to throw
|
||||
}
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
|
||||
// Simulate JSON.parse throwing for workspace settings
|
||||
vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
|
||||
throw workspaceReadError;
|
||||
});
|
||||
return invalidJsonContent;
|
||||
}
|
||||
return '{}'; // Default for other reads
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Check that settings are empty due to parsing errors
|
||||
expect(settings.user.settings).toEqual({});
|
||||
expect(settings.workspace.settings).toEqual({});
|
||||
expect(settings.merged).toEqual({});
|
||||
|
||||
// Check that error objects are populated in settings.errors
|
||||
expect(settings.errors).toBeDefined();
|
||||
// Assuming both user and workspace files cause errors and are added in order
|
||||
expect(settings.errors.length).toEqual(2);
|
||||
|
||||
const userError = settings.errors.find(
|
||||
(e) => e.path === USER_SETTINGS_PATH,
|
||||
);
|
||||
expect(userError).toBeDefined();
|
||||
expect(userError?.message).toBe(userReadError.message);
|
||||
|
||||
const workspaceError = settings.errors.find(
|
||||
(e) => e.path === MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
);
|
||||
expect(workspaceError).toBeDefined();
|
||||
expect(workspaceError?.message).toBe(workspaceReadError.message);
|
||||
|
||||
// Restore JSON.parse mock if it was spied on specifically for this test
|
||||
vi.restoreAllMocks(); // Or more targeted restore if needed
|
||||
});
|
||||
|
||||
it('should resolve environment variables in user settings', () => {
|
||||
process.env.TEST_API_KEY = 'user_api_key_from_env';
|
||||
const userSettingsContent = {
|
||||
apiKey: '$TEST_API_KEY',
|
||||
someUrl: 'https://test.com/${TEST_API_KEY}',
|
||||
};
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.user.settings.apiKey).toBe('user_api_key_from_env');
|
||||
expect(settings.user.settings.someUrl).toBe(
|
||||
'https://test.com/user_api_key_from_env',
|
||||
);
|
||||
expect(settings.merged.apiKey).toBe('user_api_key_from_env');
|
||||
delete process.env.TEST_API_KEY;
|
||||
});
|
||||
|
||||
it('should resolve environment variables in workspace settings', () => {
|
||||
process.env.WORKSPACE_ENDPOINT = 'workspace_endpoint_from_env';
|
||||
const workspaceSettingsContent = {
|
||||
endpoint: '${WORKSPACE_ENDPOINT}/api',
|
||||
nested: { value: '$WORKSPACE_ENDPOINT' },
|
||||
};
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.workspace.settings.endpoint).toBe(
|
||||
'workspace_endpoint_from_env/api',
|
||||
);
|
||||
expect(settings.workspace.settings.nested.value).toBe(
|
||||
'workspace_endpoint_from_env',
|
||||
);
|
||||
expect(settings.merged.endpoint).toBe('workspace_endpoint_from_env/api');
|
||||
delete process.env.WORKSPACE_ENDPOINT;
|
||||
});
|
||||
|
||||
it('should prioritize workspace env variables over user env variables if keys clash after resolution', () => {
|
||||
const userSettingsContent = { configValue: '$SHARED_VAR' };
|
||||
const workspaceSettingsContent = { configValue: '$SHARED_VAR' };
|
||||
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const originalSharedVar = process.env.SHARED_VAR;
|
||||
// Temporarily delete to ensure a clean slate for the test's specific manipulations
|
||||
delete process.env.SHARED_VAR;
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH) {
|
||||
process.env.SHARED_VAR = 'user_value_for_user_read'; // Set for user settings read
|
||||
return JSON.stringify(userSettingsContent);
|
||||
}
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
|
||||
process.env.SHARED_VAR = 'workspace_value_for_workspace_read'; // Set for workspace settings read
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
}
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.user.settings.configValue).toBe(
|
||||
'user_value_for_user_read',
|
||||
);
|
||||
expect(settings.workspace.settings.configValue).toBe(
|
||||
'workspace_value_for_workspace_read',
|
||||
);
|
||||
// Merged should take workspace's resolved value
|
||||
expect(settings.merged.configValue).toBe(
|
||||
'workspace_value_for_workspace_read',
|
||||
);
|
||||
|
||||
// Restore original environment variable state
|
||||
if (originalSharedVar !== undefined) {
|
||||
process.env.SHARED_VAR = originalSharedVar;
|
||||
} else {
|
||||
delete process.env.SHARED_VAR; // Ensure it's deleted if it wasn't there before
|
||||
}
|
||||
});
|
||||
|
||||
it('should leave unresolved environment variables as is', () => {
|
||||
const userSettingsContent = { apiKey: '$UNDEFINED_VAR' };
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.user.settings.apiKey).toBe('$UNDEFINED_VAR');
|
||||
expect(settings.merged.apiKey).toBe('$UNDEFINED_VAR');
|
||||
});
|
||||
|
||||
it('should resolve multiple environment variables in a single string', () => {
|
||||
process.env.VAR_A = 'valueA';
|
||||
process.env.VAR_B = 'valueB';
|
||||
const userSettingsContent = { path: '/path/$VAR_A/${VAR_B}/end' };
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.user.settings.path).toBe('/path/valueA/valueB/end');
|
||||
delete process.env.VAR_A;
|
||||
delete process.env.VAR_B;
|
||||
});
|
||||
|
||||
it('should resolve environment variables in arrays', () => {
|
||||
process.env.ITEM_1 = 'item1_env';
|
||||
process.env.ITEM_2 = 'item2_env';
|
||||
const userSettingsContent = { list: ['$ITEM_1', '${ITEM_2}', 'literal'] };
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.user.settings.list).toEqual([
|
||||
'item1_env',
|
||||
'item2_env',
|
||||
'literal',
|
||||
]);
|
||||
delete process.env.ITEM_1;
|
||||
delete process.env.ITEM_2;
|
||||
});
|
||||
|
||||
it('should correctly pass through null, boolean, and number types, and handle undefined properties', () => {
|
||||
process.env.MY_ENV_STRING = 'env_string_value';
|
||||
process.env.MY_ENV_STRING_NESTED = 'env_string_nested_value';
|
||||
|
||||
const userSettingsContent = {
|
||||
nullVal: null,
|
||||
trueVal: true,
|
||||
falseVal: false,
|
||||
numberVal: 123.45,
|
||||
stringVal: '$MY_ENV_STRING',
|
||||
nestedObj: {
|
||||
nestedNull: null,
|
||||
nestedBool: true,
|
||||
nestedNum: 0,
|
||||
nestedString: 'literal',
|
||||
anotherEnv: '${MY_ENV_STRING_NESTED}',
|
||||
},
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.user.settings.nullVal).toBeNull();
|
||||
expect(settings.user.settings.trueVal).toBe(true);
|
||||
expect(settings.user.settings.falseVal).toBe(false);
|
||||
expect(settings.user.settings.numberVal).toBe(123.45);
|
||||
expect(settings.user.settings.stringVal).toBe('env_string_value');
|
||||
expect(settings.user.settings.undefinedVal).toBeUndefined();
|
||||
|
||||
expect(settings.user.settings.nestedObj.nestedNull).toBeNull();
|
||||
expect(settings.user.settings.nestedObj.nestedBool).toBe(true);
|
||||
expect(settings.user.settings.nestedObj.nestedNum).toBe(0);
|
||||
expect(settings.user.settings.nestedObj.nestedString).toBe('literal');
|
||||
expect(settings.user.settings.nestedObj.anotherEnv).toBe(
|
||||
'env_string_nested_value',
|
||||
);
|
||||
|
||||
delete process.env.MY_ENV_STRING;
|
||||
delete process.env.MY_ENV_STRING_NESTED;
|
||||
});
|
||||
|
||||
it('should resolve multiple concatenated environment variables in a single string value', () => {
|
||||
process.env.TEST_HOST = 'myhost';
|
||||
process.env.TEST_PORT = '9090';
|
||||
const userSettingsContent = {
|
||||
serverAddress: '${TEST_HOST}:${TEST_PORT}/api',
|
||||
};
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.user.settings.serverAddress).toBe('myhost:9090/api');
|
||||
|
||||
delete process.env.TEST_HOST;
|
||||
delete process.env.TEST_PORT;
|
||||
});
|
||||
});
|
||||
|
||||
describe('LoadedSettings class', () => {
|
||||
it('setValue should update the correct scope and recompute merged settings', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(false);
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR) as LoadedSettings;
|
||||
|
||||
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
|
||||
// mkdirSync is mocked in beforeEach to return undefined, which is fine for void usage
|
||||
|
||||
loadedSettings.setValue(SettingScope.User, 'theme', 'matrix');
|
||||
expect(loadedSettings.user.settings.theme).toBe('matrix');
|
||||
expect(loadedSettings.merged.theme).toBe('matrix');
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
USER_SETTINGS_PATH,
|
||||
JSON.stringify({ theme: 'matrix' }, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
loadedSettings.setValue(
|
||||
SettingScope.Workspace,
|
||||
'contextFileName',
|
||||
'MY_AGENTS.md',
|
||||
);
|
||||
expect(loadedSettings.workspace.settings.contextFileName).toBe(
|
||||
'MY_AGENTS.md',
|
||||
);
|
||||
expect(loadedSettings.merged.contextFileName).toBe('MY_AGENTS.md');
|
||||
expect(loadedSettings.merged.theme).toBe('matrix'); // User setting should still be there
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
JSON.stringify({ contextFileName: 'MY_AGENTS.md' }, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// Workspace theme overrides user theme
|
||||
loadedSettings.setValue(SettingScope.Workspace, 'theme', 'ocean');
|
||||
|
||||
expect(loadedSettings.workspace.settings.theme).toBe('ocean');
|
||||
expect(loadedSettings.merged.theme).toBe('ocean');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { homedir } from 'os';
|
||||
import {
|
||||
MCPServerConfig,
|
||||
getErrorMessage,
|
||||
BugCommandSettings,
|
||||
TelemetrySettings,
|
||||
AuthType,
|
||||
} from '@gemini-cli/core';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { DefaultLight } from '../ui/themes/default-light.js';
|
||||
import { DefaultDark } from '../ui/themes/default.js';
|
||||
|
||||
export const SETTINGS_DIRECTORY_NAME = '.gemini';
|
||||
export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
|
||||
export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
|
||||
|
||||
export enum SettingScope {
|
||||
User = 'User',
|
||||
Workspace = 'Workspace',
|
||||
}
|
||||
|
||||
export interface CheckpointingSettings {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AccessibilitySettings {
|
||||
disableLoadingPhrases?: boolean;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
theme?: string;
|
||||
selectedAuthType?: AuthType;
|
||||
sandbox?: boolean | string;
|
||||
coreTools?: string[];
|
||||
excludeTools?: string[];
|
||||
toolDiscoveryCommand?: string;
|
||||
toolCallCommand?: string;
|
||||
mcpServerCommand?: string;
|
||||
mcpServers?: Record<string, MCPServerConfig>;
|
||||
showMemoryUsage?: boolean;
|
||||
contextFileName?: string | string[];
|
||||
accessibility?: AccessibilitySettings;
|
||||
telemetry?: TelemetrySettings;
|
||||
preferredEditor?: string;
|
||||
bugCommand?: BugCommandSettings;
|
||||
checkpointing?: CheckpointingSettings;
|
||||
|
||||
// Git-aware file filtering settings
|
||||
fileFiltering?: {
|
||||
respectGitIgnore?: boolean;
|
||||
};
|
||||
|
||||
// UI setting. Does not display the ANSI-controlled terminal title.
|
||||
hideWindowTitle?: boolean;
|
||||
|
||||
// Add other settings here.
|
||||
}
|
||||
|
||||
export interface SettingsError {
|
||||
message: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface SettingsFile {
|
||||
settings: Settings;
|
||||
path: string;
|
||||
}
|
||||
export class LoadedSettings {
|
||||
constructor(
|
||||
user: SettingsFile,
|
||||
workspace: SettingsFile,
|
||||
errors: SettingsError[],
|
||||
) {
|
||||
this.user = user;
|
||||
this.workspace = workspace;
|
||||
this.errors = errors;
|
||||
this._merged = this.computeMergedSettings();
|
||||
}
|
||||
|
||||
readonly user: SettingsFile;
|
||||
readonly workspace: SettingsFile;
|
||||
readonly errors: SettingsError[];
|
||||
|
||||
private _merged: Settings;
|
||||
|
||||
get merged(): Settings {
|
||||
return this._merged;
|
||||
}
|
||||
|
||||
private computeMergedSettings(): Settings {
|
||||
return {
|
||||
...this.user.settings,
|
||||
...this.workspace.settings,
|
||||
};
|
||||
}
|
||||
|
||||
forScope(scope: SettingScope): SettingsFile {
|
||||
switch (scope) {
|
||||
case SettingScope.User:
|
||||
return this.user;
|
||||
case SettingScope.Workspace:
|
||||
return this.workspace;
|
||||
default:
|
||||
throw new Error(`Invalid scope: ${scope}`);
|
||||
}
|
||||
}
|
||||
|
||||
setValue(
|
||||
scope: SettingScope,
|
||||
key: keyof Settings,
|
||||
value: string | Record<string, MCPServerConfig> | undefined,
|
||||
): void {
|
||||
const settingsFile = this.forScope(scope);
|
||||
// @ts-expect-error - value can be string | Record<string, MCPServerConfig>
|
||||
settingsFile.settings[key] = value;
|
||||
this._merged = this.computeMergedSettings();
|
||||
saveSettings(settingsFile);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEnvVarsInString(value: string): string {
|
||||
const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME}
|
||||
return value.replace(envVarRegex, (match, varName1, varName2) => {
|
||||
const varName = varName1 || varName2;
|
||||
if (process && process.env && typeof process.env[varName] === 'string') {
|
||||
return process.env[varName]!;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveEnvVarsInObject<T>(obj: T): T {
|
||||
if (
|
||||
obj === null ||
|
||||
obj === undefined ||
|
||||
typeof obj === 'boolean' ||
|
||||
typeof obj === 'number'
|
||||
) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return resolveEnvVarsInString(obj) as unknown as T;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const newObj = { ...obj } as T;
|
||||
for (const key in newObj) {
|
||||
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||||
newObj[key] = resolveEnvVarsInObject(newObj[key]);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads settings from user and workspace directories.
|
||||
* Project settings override user settings.
|
||||
*/
|
||||
export function loadSettings(workspaceDir: string): LoadedSettings {
|
||||
let userSettings: Settings = {};
|
||||
let workspaceSettings: Settings = {};
|
||||
const settingsErrors: SettingsError[] = [];
|
||||
|
||||
// Load user settings
|
||||
try {
|
||||
if (fs.existsSync(USER_SETTINGS_PATH)) {
|
||||
const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
|
||||
const parsedUserSettings = JSON.parse(
|
||||
stripJsonComments(userContent),
|
||||
) as Settings;
|
||||
userSettings = resolveEnvVarsInObject(parsedUserSettings);
|
||||
// Support legacy theme names
|
||||
if (userSettings.theme && userSettings.theme === 'VS') {
|
||||
userSettings.theme = DefaultLight.name;
|
||||
} else if (userSettings.theme && userSettings.theme === 'VS2015') {
|
||||
userSettings.theme = DefaultDark.name;
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: USER_SETTINGS_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
const workspaceSettingsPath = path.join(
|
||||
workspaceDir,
|
||||
SETTINGS_DIRECTORY_NAME,
|
||||
'settings.json',
|
||||
);
|
||||
|
||||
// Load workspace settings
|
||||
try {
|
||||
if (fs.existsSync(workspaceSettingsPath)) {
|
||||
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
|
||||
const parsedWorkspaceSettings = JSON.parse(
|
||||
stripJsonComments(projectContent),
|
||||
) as Settings;
|
||||
workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
|
||||
if (workspaceSettings.theme && workspaceSettings.theme === 'VS') {
|
||||
workspaceSettings.theme = DefaultLight.name;
|
||||
} else if (
|
||||
workspaceSettings.theme &&
|
||||
workspaceSettings.theme === 'VS2015'
|
||||
) {
|
||||
workspaceSettings.theme = DefaultDark.name;
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: workspaceSettingsPath,
|
||||
});
|
||||
}
|
||||
|
||||
return new LoadedSettings(
|
||||
{
|
||||
path: USER_SETTINGS_PATH,
|
||||
settings: userSettings,
|
||||
},
|
||||
{
|
||||
path: workspaceSettingsPath,
|
||||
settings: workspaceSettings,
|
||||
},
|
||||
settingsErrors,
|
||||
);
|
||||
}
|
||||
|
||||
export function saveSettings(settingsFile: SettingsFile): void {
|
||||
try {
|
||||
// Ensure the directory exists
|
||||
const dirPath = path.dirname(settingsFile.path);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
settingsFile.path,
|
||||
JSON.stringify(settingsFile.settings, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error saving user settings file:', error);
|
||||
}
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
import {
|
||||
GenerateContentConfig, GoogleGenAI, Part, Chat,
|
||||
Type,
|
||||
SchemaUnion,
|
||||
PartListUnion,
|
||||
Content
|
||||
} from '@google/genai';
|
||||
import { getApiKey } from '../config/env.js';
|
||||
import { CoreSystemPrompt } from './prompts.js';
|
||||
import { type ToolCallEvent, type ToolCallConfirmationDetails, ToolCallStatus } from '../ui/types.js';
|
||||
import process from 'node:process';
|
||||
import { toolRegistry } from '../tools/tool-registry.js';
|
||||
import { ToolResult } from '../tools/ToolResult.js';
|
||||
import { getFolderStructure } from '../utils/getFolderStructure.js';
|
||||
import { GeminiEventType, GeminiStream } from './GeminiStream.js';
|
||||
|
||||
type ToolExecutionOutcome = {
|
||||
callId: string;
|
||||
name: string;
|
||||
args: Record<string, any>;
|
||||
result?: ToolResult;
|
||||
error?: any;
|
||||
confirmationDetails?: ToolCallConfirmationDetails;
|
||||
};
|
||||
|
||||
export class GeminiClient {
|
||||
private ai: GoogleGenAI;
|
||||
private defaultHyperParameters: GenerateContentConfig = {
|
||||
temperature: 0,
|
||||
topP: 1,
|
||||
};
|
||||
private readonly MAX_TURNS = 100;
|
||||
|
||||
constructor() {
|
||||
const apiKey = getApiKey();
|
||||
this.ai = new GoogleGenAI({ apiKey });
|
||||
}
|
||||
|
||||
public async startChat(): Promise<Chat> {
|
||||
const tools = toolRegistry.getToolSchemas();
|
||||
|
||||
// --- Get environmental information ---
|
||||
const cwd = process.cwd();
|
||||
const today = new Date().toLocaleDateString(undefined, { // Use locale-aware date formatting
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||
});
|
||||
const platform = process.platform;
|
||||
|
||||
// --- Format information into a conversational multi-line string ---
|
||||
const folderStructure = await getFolderStructure(cwd);
|
||||
// --- End folder structure formatting ---)
|
||||
const initialContextText = `
|
||||
Okay, just setting up the context for our chat.
|
||||
Today is ${today}.
|
||||
My operating system is: ${platform}
|
||||
I'm currently working in the directory: ${cwd}
|
||||
${folderStructure}
|
||||
`.trim();
|
||||
|
||||
const initialContextPart: Part = { text: initialContextText };
|
||||
// --- End environmental information formatting ---
|
||||
|
||||
try {
|
||||
const chat = this.ai.chats.create({
|
||||
model: 'gemini-2.5-pro-preview-03-25',//'gemini-2.0-flash',
|
||||
config: {
|
||||
systemInstruction: CoreSystemPrompt,
|
||||
...this.defaultHyperParameters,
|
||||
tools,
|
||||
},
|
||||
history: [
|
||||
// --- Add the context as a single part in the initial user message ---
|
||||
{
|
||||
role: "user",
|
||||
parts: [initialContextPart] // Pass the single Part object in an array
|
||||
},
|
||||
// --- Add an empty model response to balance the history ---
|
||||
{
|
||||
role: "model",
|
||||
parts: [{ text: "Got it. Thanks for the context!" }] // A slightly more conversational model response
|
||||
}
|
||||
// --- End history modification ---
|
||||
],
|
||||
});
|
||||
return chat;
|
||||
} catch (error) {
|
||||
console.error("Error initializing Gemini chat session:", error);
|
||||
const message = error instanceof Error ? error.message : "Unknown error.";
|
||||
throw new Error(`Failed to initialize chat: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public addMessageToHistory(chat: Chat, message: Content): void {
|
||||
const history = chat.getHistory();
|
||||
history.push(message);
|
||||
this.ai.chats
|
||||
chat
|
||||
}
|
||||
|
||||
public async* sendMessageStream(
|
||||
chat: Chat,
|
||||
request: PartListUnion,
|
||||
signal?: AbortSignal
|
||||
): GeminiStream {
|
||||
let currentMessageToSend: PartListUnion = request;
|
||||
let turns = 0;
|
||||
|
||||
try {
|
||||
while (turns < this.MAX_TURNS) {
|
||||
turns++;
|
||||
const resultStream = await chat.sendMessageStream({ message: currentMessageToSend });
|
||||
let functionResponseParts: Part[] = [];
|
||||
let pendingToolCalls: Array<{ callId: string; name: string; args: Record<string, any> }> = [];
|
||||
let yieldedTextInTurn = false;
|
||||
const chunksForDebug = [];
|
||||
|
||||
for await (const chunk of resultStream) {
|
||||
chunksForDebug.push(chunk);
|
||||
if (signal?.aborted) {
|
||||
const abortError = new Error("Request cancelled by user during stream.");
|
||||
abortError.name = 'AbortError';
|
||||
throw abortError;
|
||||
}
|
||||
|
||||
const functionCalls = chunk.functionCalls;
|
||||
if (functionCalls && functionCalls.length > 0) {
|
||||
for (const call of functionCalls) {
|
||||
const callId = call.id ?? `${call.name}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const name = call.name || 'undefined_tool_name';
|
||||
const args = (call.args || {}) as Record<string, any>;
|
||||
|
||||
pendingToolCalls.push({ callId, name, args });
|
||||
const evtValue: ToolCallEvent = {
|
||||
type: 'tool_call',
|
||||
status: ToolCallStatus.Pending,
|
||||
callId,
|
||||
name,
|
||||
args,
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: undefined,
|
||||
}
|
||||
yield {
|
||||
type: GeminiEventType.ToolCallInfo,
|
||||
value: evtValue,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const text = chunk.text;
|
||||
if (text) {
|
||||
yieldedTextInTurn = true;
|
||||
yield {
|
||||
type: GeminiEventType.Content,
|
||||
value: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingToolCalls.length > 0) {
|
||||
const toolPromises: Promise<ToolExecutionOutcome>[] = pendingToolCalls.map(async pendingToolCall => {
|
||||
const tool = toolRegistry.getTool(pendingToolCall.name);
|
||||
|
||||
if (!tool) {
|
||||
// Directly return error outcome if tool not found
|
||||
return { ...pendingToolCall, error: new Error(`Tool "${pendingToolCall.name}" not found or is not registered.`) };
|
||||
}
|
||||
|
||||
try {
|
||||
const confirmation = await tool.shouldConfirmExecute(pendingToolCall.args);
|
||||
if (confirmation) {
|
||||
return { ...pendingToolCall, confirmationDetails: confirmation };
|
||||
}
|
||||
} catch (error) {
|
||||
return { ...pendingToolCall, error: new Error(`Tool failed to check tool confirmation: ${error}`) };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await tool.execute(pendingToolCall.args);
|
||||
return { ...pendingToolCall, result };
|
||||
} catch (error) {
|
||||
return { ...pendingToolCall, error: new Error(`Tool failed to execute: ${error}`) };
|
||||
}
|
||||
});
|
||||
const toolExecutionOutcomes: ToolExecutionOutcome[] = await Promise.all(toolPromises);
|
||||
|
||||
for (const executedTool of toolExecutionOutcomes) {
|
||||
const { callId, name, args, result, error, confirmationDetails } = executedTool;
|
||||
|
||||
if (error) {
|
||||
const errorMessage = error?.message || String(error);
|
||||
yield {
|
||||
type: GeminiEventType.Content,
|
||||
value: `[Error invoking tool ${name}: ${errorMessage}]`,
|
||||
};
|
||||
} else if (result && typeof result === 'object' && result !== null && 'error' in result) {
|
||||
const errorMessage = String(result.error);
|
||||
yield {
|
||||
type: GeminiEventType.Content,
|
||||
value: `[Error executing tool ${name}: ${errorMessage}]`,
|
||||
};
|
||||
} else {
|
||||
const status = confirmationDetails ? ToolCallStatus.Confirming : ToolCallStatus.Invoked;
|
||||
const evtValue: ToolCallEvent = { type: 'tool_call', status, callId, name, args, resultDisplay: result?.returnDisplay, confirmationDetails }
|
||||
yield {
|
||||
type: GeminiEventType.ToolCallInfo,
|
||||
value: evtValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pendingToolCalls = [];
|
||||
|
||||
const waitingOnConfirmations = toolExecutionOutcomes.filter(outcome => outcome.confirmationDetails).length > 0;
|
||||
if (waitingOnConfirmations) {
|
||||
// Stop processing content, wait for user.
|
||||
// TODO: Kill token processing once API supports signals.
|
||||
break;
|
||||
}
|
||||
|
||||
functionResponseParts = toolExecutionOutcomes.map((executedTool: ToolExecutionOutcome): Part => {
|
||||
const { name, result, error } = executedTool;
|
||||
const output = { "output": result?.llmContent };
|
||||
let toolOutcomePayload: any;
|
||||
|
||||
if (error) {
|
||||
const errorMessage = error?.message || String(error);
|
||||
toolOutcomePayload = { error: `Invocation failed: ${errorMessage}` };
|
||||
console.error(`[Turn ${turns}] Critical error invoking tool ${name}:`, error);
|
||||
} else if (result && typeof result === 'object' && result !== null && 'error' in result) {
|
||||
toolOutcomePayload = output;
|
||||
console.warn(`[Turn ${turns}] Tool ${name} returned an error structure:`, result.error);
|
||||
} else {
|
||||
toolOutcomePayload = output;
|
||||
}
|
||||
|
||||
return {
|
||||
functionResponse: {
|
||||
name: name,
|
||||
id: executedTool.callId,
|
||||
response: toolOutcomePayload,
|
||||
},
|
||||
};
|
||||
});
|
||||
currentMessageToSend = functionResponseParts;
|
||||
} else if (yieldedTextInTurn) {
|
||||
const history = chat.getHistory();
|
||||
const checkPrompt = `Analyze *only* the content and structure of your immediately preceding response (your last turn in the conversation history). Based *strictly* on that response, determine who should logically speak next: the 'user' or the 'model' (you).
|
||||
|
||||
**Decision Rules (apply in order):**
|
||||
|
||||
1. **Model Continues:** If your last response explicitly states an immediate next action *you* intend to take (e.g., "Next, I will...", "Now I'll process...", "Moving on to analyze...", indicates an intended tool call that didn't execute), OR if the response seems clearly incomplete (cut off mid-thought without a natural conclusion), then the **'model'** should speak next.
|
||||
2. **Question to User:** If your last response ends with a direct question specifically addressed *to the user*, then the **'user'** should speak next.
|
||||
3. **Waiting for User:** If your last response completed a thought, statement, or task *and* does not meet the criteria for Rule 1 (Model Continues) or Rule 2 (Question to User), it implies a pause expecting user input or reaction. In this case, the **'user'** should speak next.
|
||||
|
||||
**Output Format:**
|
||||
|
||||
Respond *only* in JSON format according to the following schema. Do not include any text outside the JSON structure.
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reasoning": {
|
||||
"type": "string",
|
||||
"description": "Brief explanation justifying the 'next_speaker' choice based *strictly* on the applicable rule and the content/structure of the preceding turn."
|
||||
},
|
||||
"next_speaker": {
|
||||
"type": "string",
|
||||
"enum": ["user", "model"],
|
||||
"description": "Who should speak next based *only* on the preceding turn and the decision rules."
|
||||
}
|
||||
},
|
||||
"required": ["next_speaker", "reasoning"]
|
||||
\`\`\`
|
||||
}`;
|
||||
|
||||
// Schema Idea
|
||||
const responseSchema: SchemaUnion = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
reasoning: {
|
||||
type: Type.STRING,
|
||||
description: "Brief explanation justifying the 'next_speaker' choice based *strictly* on the applicable rule and the content/structure of the preceding turn."
|
||||
},
|
||||
next_speaker: {
|
||||
type: Type.STRING,
|
||||
enum: ['user', 'model'], // Enforce the choices
|
||||
description: "Who should speak next based *only* on the preceding turn and the decision rules",
|
||||
},
|
||||
},
|
||||
required: ['reasoning', 'next_speaker']
|
||||
};
|
||||
|
||||
try {
|
||||
// Use the new generateJson method, passing the history and the check prompt
|
||||
const parsedResponse = await this.generateJson([...history, { role: "user", parts: [{ text: checkPrompt }] }], responseSchema);
|
||||
|
||||
// Safely extract the next speaker value
|
||||
const nextSpeaker: string | undefined = typeof parsedResponse?.next_speaker === 'string' ? parsedResponse.next_speaker : undefined;
|
||||
|
||||
if (nextSpeaker === 'model') {
|
||||
currentMessageToSend = { text: 'alright' }; // Or potentially a more meaningful continuation prompt
|
||||
} else {
|
||||
// 'user' should speak next, or value is missing/invalid. End the turn.
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Turn ${turns}] Failed to get or parse next speaker check:`, error);
|
||||
// If the check fails, assume user should speak next to avoid infinite loops
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
console.warn(`[Turn ${turns}] No text or function calls received from Gemini. Ending interaction.`);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (turns >= this.MAX_TURNS) {
|
||||
console.warn("sendMessageStream: Reached maximum tool call turns limit.");
|
||||
yield {
|
||||
type: GeminiEventType.Content,
|
||||
value: "\n\n[System Notice: Maximum interaction turns reached. The conversation may be incomplete.]",
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
console.log("Gemini stream request aborted by user.");
|
||||
throw error;
|
||||
} else {
|
||||
console.error(`Error during Gemini stream or tool interaction:`, error);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
yield {
|
||||
type: GeminiEventType.Content,
|
||||
value: `\n\n[Error: An unexpected error occurred during the chat: ${message}]`,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates structured JSON content based on conversational history and a schema.
|
||||
* @param contents The conversational history (Content array) to provide context.
|
||||
* @param schema The SchemaUnion defining the desired JSON structure.
|
||||
* @returns A promise that resolves to the parsed JSON object matching the schema.
|
||||
* @throws Throws an error if the API call fails or the response is not valid JSON.
|
||||
*/
|
||||
public async generateJson(contents: Content[], schema: SchemaUnion): Promise<any> {
|
||||
try {
|
||||
const result = await this.ai.models.generateContent({
|
||||
model: 'gemini-2.0-flash', // Using flash for potentially faster structured output
|
||||
config: {
|
||||
...this.defaultHyperParameters,
|
||||
systemInstruction: CoreSystemPrompt,
|
||||
responseSchema: schema,
|
||||
responseMimeType: 'application/json',
|
||||
},
|
||||
contents: contents, // Pass the full Content array
|
||||
});
|
||||
|
||||
const responseText = result.text;
|
||||
if (!responseText) {
|
||||
throw new Error("API returned an empty response.");
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedJson = JSON.parse(responseText);
|
||||
// TODO: Add schema validation if needed
|
||||
return parsedJson;
|
||||
} catch (parseError) {
|
||||
console.error("Failed to parse JSON response:", responseText);
|
||||
throw new Error(`Failed to parse API response as JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error generating JSON content:", error);
|
||||
const message = error instanceof Error ? error.message : "Unknown API error.";
|
||||
throw new Error(`Failed to generate JSON content: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ToolCallEvent } from "../ui/types.js";
|
||||
|
||||
export enum GeminiEventType {
|
||||
Content,
|
||||
ToolCallInfo,
|
||||
}
|
||||
|
||||
export interface GeminiContentEvent {
|
||||
type: GeminiEventType.Content;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface GeminiToolCallInfoEvent {
|
||||
type: GeminiEventType.ToolCallInfo;
|
||||
value: ToolCallEvent;
|
||||
}
|
||||
|
||||
export type GeminiEvent =
|
||||
| GeminiContentEvent
|
||||
| GeminiToolCallInfoEvent;
|
||||
|
||||
export type GeminiStream = AsyncIterable<GeminiEvent>;
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum StreamingState {
|
||||
Idle,
|
||||
Responding,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const MEMORY_FILE_NAME = 'GEMINI.md';
|
||||
@@ -1,142 +0,0 @@
|
||||
import { Part } from '@google/genai';
|
||||
import { HistoryItem } from '../ui/types.js';
|
||||
import { GeminiEventType, GeminiStream } from './GeminiStream.js';
|
||||
import { handleToolCallChunk, addErrorMessageToHistory } from './historyUpdater.js';
|
||||
|
||||
interface StreamProcessorParams {
|
||||
stream: GeminiStream;
|
||||
signal: AbortSignal;
|
||||
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>;
|
||||
submitQuery: (query: Part) => Promise<void>,
|
||||
getNextMessageId: () => number;
|
||||
addHistoryItem: (itemData: Omit<HistoryItem, 'id'>, id: number) => void;
|
||||
currentToolGroupIdRef: React.MutableRefObject<number | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the Gemini stream, managing text buffering, adaptive rendering,
|
||||
* and delegating history updates for tool calls and errors.
|
||||
*/
|
||||
export const processGeminiStream = async ({ // Renamed function for clarity
|
||||
stream,
|
||||
signal,
|
||||
setHistory,
|
||||
submitQuery,
|
||||
getNextMessageId,
|
||||
addHistoryItem,
|
||||
currentToolGroupIdRef,
|
||||
}: StreamProcessorParams): Promise<void> => {
|
||||
// --- State specific to this stream processing invocation ---
|
||||
let textBuffer = '';
|
||||
let renderTimeoutId: NodeJS.Timeout | null = null;
|
||||
let isStreamComplete = false;
|
||||
let currentGeminiMessageId: number | null = null;
|
||||
|
||||
const render = (content: string) => {
|
||||
if (currentGeminiMessageId === null) {
|
||||
return;
|
||||
}
|
||||
setHistory(prev => prev.map(item =>
|
||||
item.id === currentGeminiMessageId && item.type === 'gemini'
|
||||
? { ...item, text: (item.text ?? '') + content }
|
||||
: item
|
||||
));
|
||||
}
|
||||
// --- Adaptive Rendering Logic (nested) ---
|
||||
const renderBufferedText = () => {
|
||||
if (signal.aborted) {
|
||||
if (renderTimeoutId) clearTimeout(renderTimeoutId);
|
||||
renderTimeoutId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const bufferLength = textBuffer.length;
|
||||
let chunkSize = 0;
|
||||
let delay = 50;
|
||||
|
||||
if (bufferLength > 150) {
|
||||
chunkSize = Math.min(bufferLength, 30); delay = 5;
|
||||
} else if (bufferLength > 30) {
|
||||
chunkSize = Math.min(bufferLength, 10); delay = 10;
|
||||
} else if (bufferLength > 0) {
|
||||
chunkSize = 2; delay = 20;
|
||||
}
|
||||
|
||||
if (chunkSize > 0) {
|
||||
const chunkToRender = textBuffer.substring(0, chunkSize);
|
||||
textBuffer = textBuffer.substring(chunkSize);
|
||||
render(chunkToRender);
|
||||
|
||||
renderTimeoutId = setTimeout(renderBufferedText, delay);
|
||||
} else {
|
||||
renderTimeoutId = null; // Clear timeout ID if nothing to render
|
||||
if (!isStreamComplete) {
|
||||
// Buffer empty, but stream might still send data, check again later
|
||||
renderTimeoutId = setTimeout(renderBufferedText, 50);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleRender = () => {
|
||||
if (renderTimeoutId === null) {
|
||||
renderTimeoutId = setTimeout(renderBufferedText, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Stream Processing Loop ---
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
if (signal.aborted) break;
|
||||
|
||||
if (chunk.type === GeminiEventType.Content) {
|
||||
currentToolGroupIdRef.current = null; // Reset tool group on text
|
||||
|
||||
if (currentGeminiMessageId === null) {
|
||||
currentGeminiMessageId = getNextMessageId();
|
||||
addHistoryItem({ type: 'gemini', text: '' }, currentGeminiMessageId);
|
||||
textBuffer = '';
|
||||
}
|
||||
textBuffer += chunk.value;
|
||||
scheduleRender();
|
||||
|
||||
} else if (chunk.type === GeminiEventType.ToolCallInfo) {
|
||||
if (renderTimeoutId) { // Stop rendering loop
|
||||
clearTimeout(renderTimeoutId);
|
||||
renderTimeoutId = null;
|
||||
}
|
||||
|
||||
// Flush any text buffer content.
|
||||
render(textBuffer);
|
||||
currentGeminiMessageId = null; // End text message context
|
||||
textBuffer = ''; // Clear buffer
|
||||
|
||||
// Delegate history update for tool call
|
||||
handleToolCallChunk(
|
||||
chunk.value,
|
||||
setHistory,
|
||||
submitQuery,
|
||||
getNextMessageId,
|
||||
currentToolGroupIdRef
|
||||
);
|
||||
}
|
||||
}
|
||||
if (signal.aborted) {
|
||||
throw new Error("Request cancelled by user");
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (renderTimeoutId) { // Ensure render loop stops on error
|
||||
clearTimeout(renderTimeoutId);
|
||||
renderTimeoutId = null;
|
||||
}
|
||||
// Delegate history update for error message
|
||||
addErrorMessageToHistory(error, setHistory, getNextMessageId);
|
||||
} finally {
|
||||
isStreamComplete = true; // Signal stream end for render loop completion
|
||||
if (renderTimeoutId) {
|
||||
clearTimeout(renderTimeoutId);
|
||||
renderTimeoutId = null;
|
||||
}
|
||||
|
||||
renderBufferedText(); // Force final render
|
||||
}
|
||||
};
|
||||
@@ -1,173 +0,0 @@
|
||||
import { Part } from "@google/genai";
|
||||
import { toolRegistry } from "../tools/tool-registry.js";
|
||||
import { HistoryItem, IndividualToolCallDisplay, ToolCallEvent, ToolCallStatus, ToolConfirmationOutcome, ToolEditConfirmationDetails, ToolExecuteConfirmationDetails } from "../ui/types.js";
|
||||
import { ToolResultDisplay } from "../tools/ToolResult.js";
|
||||
|
||||
/**
|
||||
* Processes a tool call chunk and updates the history state accordingly.
|
||||
* Manages adding new tool groups or updating existing ones.
|
||||
* Resides here as its primary effect is updating history based on tool events.
|
||||
*/
|
||||
export const handleToolCallChunk = (
|
||||
chunk: ToolCallEvent,
|
||||
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
|
||||
submitQuery: (query: Part) => Promise<void>,
|
||||
getNextMessageId: () => number,
|
||||
currentToolGroupIdRef: React.MutableRefObject<number | null>
|
||||
): void => {
|
||||
const toolDefinition = toolRegistry.getTool(chunk.name);
|
||||
const description = toolDefinition?.getDescription
|
||||
? toolDefinition.getDescription(chunk.args)
|
||||
: '';
|
||||
const toolDisplayName = toolDefinition?.displayName ?? chunk.name;
|
||||
let confirmationDetails = chunk.confirmationDetails;
|
||||
if (confirmationDetails) {
|
||||
const originalConfirmationDetails = confirmationDetails;
|
||||
const historyUpdatingConfirm = async (outcome: ToolConfirmationOutcome) => {
|
||||
originalConfirmationDetails.onConfirm(outcome);
|
||||
|
||||
if (outcome === ToolConfirmationOutcome.Cancel) {
|
||||
let resultDisplay: ToolResultDisplay | undefined;
|
||||
if ('fileDiff' in originalConfirmationDetails) {
|
||||
resultDisplay = { fileDiff: (originalConfirmationDetails as ToolEditConfirmationDetails).fileDiff };
|
||||
} else {
|
||||
resultDisplay = `~~${(originalConfirmationDetails as ToolExecuteConfirmationDetails).command}~~`;
|
||||
}
|
||||
handleToolCallChunk({ ...chunk, status: ToolCallStatus.Canceled, confirmationDetails: undefined, resultDisplay, }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef);
|
||||
const functionResponse: Part = {
|
||||
functionResponse: {
|
||||
name: chunk.name,
|
||||
response: { "error": "User rejected function call." },
|
||||
},
|
||||
}
|
||||
await submitQuery(functionResponse);
|
||||
} else {
|
||||
const tool = toolRegistry.getTool(chunk.name)
|
||||
if (!tool) {
|
||||
throw new Error(`Tool "${chunk.name}" not found or is not registered.`);
|
||||
}
|
||||
|
||||
handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: "Executing...", confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef);
|
||||
|
||||
const result = await tool.execute(chunk.args);
|
||||
|
||||
handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: result.returnDisplay, confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef);
|
||||
|
||||
const functionResponse: Part = {
|
||||
functionResponse: {
|
||||
name: chunk.name,
|
||||
id: chunk.callId,
|
||||
response: { "output": result.llmContent },
|
||||
},
|
||||
}
|
||||
|
||||
await submitQuery(functionResponse);
|
||||
}
|
||||
}
|
||||
|
||||
confirmationDetails = {
|
||||
...originalConfirmationDetails,
|
||||
onConfirm: historyUpdatingConfirm,
|
||||
};
|
||||
}
|
||||
const toolDetail: IndividualToolCallDisplay = {
|
||||
callId: chunk.callId,
|
||||
name: toolDisplayName,
|
||||
description,
|
||||
resultDisplay: chunk.resultDisplay,
|
||||
status: chunk.status,
|
||||
confirmationDetails: confirmationDetails,
|
||||
};
|
||||
|
||||
const activeGroupId = currentToolGroupIdRef.current;
|
||||
setHistory(prev => {
|
||||
if (chunk.status === ToolCallStatus.Pending) {
|
||||
if (activeGroupId === null) {
|
||||
// Start a new tool group
|
||||
const newGroupId = getNextMessageId();
|
||||
currentToolGroupIdRef.current = newGroupId;
|
||||
return [
|
||||
...prev,
|
||||
{ id: newGroupId, type: 'tool_group', tools: [toolDetail] } as HistoryItem
|
||||
];
|
||||
}
|
||||
|
||||
// Add to existing tool group
|
||||
return prev.map(item =>
|
||||
item.id === activeGroupId && item.type === 'tool_group'
|
||||
? item.tools.some(t => t.callId === toolDetail.callId)
|
||||
? item // Tool already listed as pending
|
||||
: { ...item, tools: [...item.tools, toolDetail] }
|
||||
: item
|
||||
);
|
||||
}
|
||||
|
||||
// Update the status of a pending tool within the active group
|
||||
if (activeGroupId === null) {
|
||||
// Log if an invoked tool arrives without an active group context
|
||||
console.warn("Received invoked tool status without an active tool group ID:", chunk);
|
||||
return prev;
|
||||
}
|
||||
|
||||
return prev.map(item =>
|
||||
item.id === activeGroupId && item.type === 'tool_group'
|
||||
? {
|
||||
...item,
|
||||
tools: item.tools.map(t =>
|
||||
t.callId === toolDetail.callId
|
||||
? { ...t, ...toolDetail, status: chunk.status } // Update details & status
|
||||
: t
|
||||
)
|
||||
}
|
||||
: item
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends an error or informational message to the history, attempting to attach
|
||||
* it to the last non-user message or creating a new entry.
|
||||
*/
|
||||
export const addErrorMessageToHistory = (
|
||||
error: any,
|
||||
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
|
||||
getNextMessageId: () => number
|
||||
): void => {
|
||||
const isAbort = error.name === 'AbortError';
|
||||
const errorType = isAbort ? 'info' : 'error';
|
||||
const errorText = isAbort
|
||||
? '[Request cancelled by user]'
|
||||
: `[Error: ${error.message || 'Unknown error'}]`;
|
||||
|
||||
setHistory(prev => {
|
||||
const reversedHistory = [...prev].reverse();
|
||||
// Find the last message that isn't from the user to append the error/info to
|
||||
const lastBotMessageIndex = reversedHistory.findIndex(item => item.type !== 'user');
|
||||
const originalIndex = lastBotMessageIndex !== -1 ? prev.length - 1 - lastBotMessageIndex : -1;
|
||||
|
||||
if (originalIndex !== -1) {
|
||||
// Append error to the last relevant message
|
||||
return prev.map((item, index) => {
|
||||
if (index === originalIndex) {
|
||||
let baseText = '';
|
||||
// Determine base text based on item type
|
||||
if (item.type === 'gemini') baseText = item.text ?? '';
|
||||
else if (item.type === 'tool_group') baseText = `Tool execution (${item.tools.length} calls)`;
|
||||
else if (item.type === 'error' || item.type === 'info') baseText = item.text ?? '';
|
||||
// Safely handle potential undefined text
|
||||
|
||||
const updatedText = (baseText + (baseText && !baseText.endsWith('\n') ? '\n' : '') + errorText).trim();
|
||||
// Reuse existing ID, update type and text
|
||||
return { ...item, type: errorType, text: updatedText };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// No previous message to append to, add a new error item
|
||||
return [
|
||||
...prev,
|
||||
{ id: getNextMessageId(), type: errorType, text: errorText } as HistoryItem
|
||||
];
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
import { ReadFileTool } from "../tools/read-file.tool.js";
|
||||
import { TerminalTool } from "../tools/terminal.tool.js";
|
||||
import { MEMORY_FILE_NAME } from "./constants.js";
|
||||
|
||||
const contactEmail = 'ntaylormullen@google.com';
|
||||
export const CoreSystemPrompt = `
|
||||
You are an interactive CLI tool assistant specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.
|
||||
|
||||
# Core Directives & Safety Rules
|
||||
1. **Explain Critical Commands:** Before executing any command (especially using \`${TerminalTool.Name}\`) that modifies the file system, codebase, or system state, you *must* provide a brief explanation of the command's purpose and potential impact. Prioritize user understanding and safety.
|
||||
2. **NEVER Commit Changes:** Unless explicitly instructed by the user to do so, you MUST NOT commit changes to version control (e.g., git commit). This is critical for user control over their repository.
|
||||
3. **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
|
||||
|
||||
# Primary Workflow: Software Engineering Tasks
|
||||
When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
|
||||
1. **Understand:** Analyze the user's request and the relevant codebase context. Check for project-specific information in \`${MEMORY_FILE_NAME}\` if it exists. Use search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions.
|
||||
2. **Implement:** Use the available tools (e.g., file editing, \`${TerminalTool.Name}\`) to construct the solution, strictly adhering to the project's established conventions (see 'Following Conventions' below).
|
||||
- If creating a new project rely on scaffolding commands do lay out the initial project structure (i.e. npm init ...)
|
||||
3. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining \`README\` files, \`${MEMORY_FILE_NAME}\`, build/package configuration (e.g., \`package.json\`), or existing test execution patterns. NEVER assume standard test commands.
|
||||
4. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific linting and type-checking commands (e.g., \`npm run lint\`, \`ruff check .\`, \`tsc\`) that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, ask the user and propose adding them to \`${MEMORY_FILE_NAME}\` for future reference.
|
||||
|
||||
# Key Operating Principles
|
||||
|
||||
## Following Conventions
|
||||
Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code and configuration first.
|
||||
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like \`package.json\`, \`Cargo.toml\`, \`requirements.txt\`, \`build.gradle\`, etc., or observe neighboring files) before employing it.
|
||||
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add comments if necessary for clarity or if requested by the user.
|
||||
|
||||
## Memory (${MEMORY_FILE_NAME})
|
||||
Utilize the \`${MEMORY_FILE_NAME}\` file in the current working directory for project-specific context:
|
||||
- Reference stored commands, style preferences, and codebase notes when performing tasks.
|
||||
- When you discover frequently used commands (build, test, lint, typecheck) or learn about specific project conventions or style preferences, proactively propose adding them to \`${MEMORY_FILE_NAME}\` for future sessions.
|
||||
|
||||
## Tone and Style (CLI Interaction)
|
||||
- **Concise & Direct:** Adopt a professional, direct, and concise tone suitable for a CLI environment.
|
||||
- **Minimal Output:** Aim for fewer than 4 lines of text output (excluding tool use/code generation) per response whenever practical. Focus strictly on the user's query.
|
||||
- **Clarity over Brevity (When Needed):** While conciseness is key, prioritize clarity for essential explanations (like pre-command warnings) or when seeking necessary clarification if a request is ambiguous.
|
||||
- **No Chitchat:** Avoid conversational filler, preambles ("Okay, I will now..."), or postambles ("I have finished the changes..."). Get straight to the action or answer.
|
||||
- **Formatting:** Use GitHub-flavored Markdown. Responses will be rendered in monospace.
|
||||
- **Tools vs. Text:** Use tools for actions, text output *only* for communication. Do not add explanatory comments within tool calls or code blocks unless specifically part of the required code/command itself.
|
||||
- **Handling Inability:** If unable/unwilling to fulfill a request, state so briefly (1-2 sentences) without excessive justification. Offer alternatives if appropriate.
|
||||
|
||||
## Proactiveness
|
||||
- **Act within Scope:** Fulfill the user's request thoroughly, including reasonable, directly implied follow-up actions.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Stop After Action:** After completing a code modification or file operation, simply stop. Do not provide summaries unless asked.
|
||||
|
||||
# Tool Usage
|
||||
- **Search:** Prefer the Agent tool for file searching to optimize context usage.
|
||||
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible.
|
||||
- **Command Execution:** Use the \`${TerminalTool.Name}\` tool for running shell commands, remembering the safety rule to explain modifying commands first.
|
||||
|
||||
# Interaction Details
|
||||
- **Help Command:** Use \`/help\` to display Gemini Code help. To get specific command/flag info, execute \`gemini -h\` via \`${TerminalTool.Name}\` and show the output.
|
||||
- **Synthetic Messages:** Ignore system messages like \`++Request Cancelled++\`. Do not generate them.
|
||||
- **Feedback:** Direct feedback to ${contactEmail}.
|
||||
|
||||
# Examples (Illustrating Tone and Workflow)
|
||||
<example>
|
||||
user: 1 + 2
|
||||
assistant: 3
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: is 13 a prime number?
|
||||
assistant: true
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: List files here.
|
||||
assistant: [tool_call: execute_bash_command for 'ls -la']))]
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: Refactor the auth logic in src/auth.py to use the 'requests' library.
|
||||
assistant: Okay, I see src/auth.py currently uses 'urllib'. Before changing it, I need to check if 'requests' is already a project dependency. [tool_call: ${TerminalTool.Name} for grep 'requests', 'requirements.txt']
|
||||
(After confirming dependency or asking user to add it)
|
||||
Okay, 'requests' is available. I will now refactor src/auth.py.
|
||||
[tool_call: Uses read, edit tools following conventions]
|
||||
(After editing)
|
||||
[tool_call: Runs project-specific lint/typecheck commands found previously, e.g., ${TerminalTool.Name} for 'ruff', 'check', 'src/auth.py']
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: Delete the temp directory.
|
||||
assistant: I can run \`rm -rf ./temp\`. This will permanently delete the directory and all its contents. Is it okay to proceed?
|
||||
</example>
|
||||
|
||||
# Final Reminder
|
||||
Your core function is efficient and safe assistance. Balance extreme conciseness with the crucial need for clarity, especially regarding safety and potential system modifications. Always prioritize user control and project conventions. Never make assumptions on the contents of files; instead use the ${ReadFileTool.Name} to ensure you aren't making too broad of assumptions.
|
||||
`;
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { main } from './gemini.js';
|
||||
import {
|
||||
LoadedSettings,
|
||||
SettingsFile,
|
||||
loadSettings,
|
||||
} from './config/settings.js';
|
||||
|
||||
// Custom error to identify mock process.exit calls
|
||||
class MockProcessExitError extends Error {
|
||||
constructor(readonly code?: string | number | null | undefined) {
|
||||
super('PROCESS_EXIT_MOCKED');
|
||||
this.name = 'MockProcessExitError';
|
||||
}
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('./config/settings.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('./config/settings.js')>();
|
||||
return {
|
||||
...actual,
|
||||
loadSettings: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./config/config.js', () => ({
|
||||
loadCliConfig: vi.fn().mockResolvedValue({
|
||||
config: {
|
||||
getSandbox: vi.fn(() => false),
|
||||
getQuestion: vi.fn(() => ''),
|
||||
},
|
||||
modelWasSwitched: false,
|
||||
originalModelBeforeSwitch: null,
|
||||
finalModel: 'test-model',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('read-package-up', () => ({
|
||||
readPackageUp: vi.fn().mockResolvedValue({
|
||||
packageJson: { name: 'test-pkg', version: 'test-version' },
|
||||
path: '/fake/path/package.json',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('update-notifier', () => ({
|
||||
default: vi.fn(() => ({
|
||||
notify: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('./utils/sandbox.js', () => ({
|
||||
sandbox_command: vi.fn(() => ''), // Default to no sandbox command
|
||||
start_sandbox: vi.fn(() => Promise.resolve()), // Mock as an async function that resolves
|
||||
}));
|
||||
|
||||
describe('gemini.tsx main function', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let loadSettingsMock: ReturnType<typeof vi.mocked<typeof loadSettings>>;
|
||||
let originalEnvGeminiSandbox: string | undefined;
|
||||
let originalEnvSandbox: string | undefined;
|
||||
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((code) => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
loadSettingsMock = vi.mocked(loadSettings);
|
||||
|
||||
// Store and clear sandbox-related env variables to ensure a consistent test environment
|
||||
originalEnvGeminiSandbox = process.env.GEMINI_SANDBOX;
|
||||
originalEnvSandbox = process.env.SANDBOX;
|
||||
delete process.env.GEMINI_SANDBOX;
|
||||
delete process.env.SANDBOX;
|
||||
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original env variables
|
||||
if (originalEnvGeminiSandbox !== undefined) {
|
||||
process.env.GEMINI_SANDBOX = originalEnvGeminiSandbox;
|
||||
} else {
|
||||
delete process.env.GEMINI_SANDBOX;
|
||||
}
|
||||
if (originalEnvSandbox !== undefined) {
|
||||
process.env.SANDBOX = originalEnvSandbox;
|
||||
} else {
|
||||
delete process.env.SANDBOX;
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call process.exit(1) if settings have errors', async () => {
|
||||
const settingsError = {
|
||||
message: 'Test settings error',
|
||||
path: '/test/settings.json',
|
||||
};
|
||||
const userSettingsFile: SettingsFile = {
|
||||
path: '/user/settings.json',
|
||||
settings: {},
|
||||
};
|
||||
const workspaceSettingsFile: SettingsFile = {
|
||||
path: '/workspace/.gemini/settings.json',
|
||||
settings: {},
|
||||
};
|
||||
const mockLoadedSettings = new LoadedSettings(
|
||||
userSettingsFile,
|
||||
workspaceSettingsFile,
|
||||
[settingsError],
|
||||
);
|
||||
|
||||
loadSettingsMock.mockReturnValue(mockLoadedSettings);
|
||||
|
||||
try {
|
||||
await main();
|
||||
// If main completes without throwing, the test should fail because process.exit was expected
|
||||
expect.fail('main function did not exit as expected');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(MockProcessExitError);
|
||||
if (error instanceof MockProcessExitError) {
|
||||
expect(error.code).toBe(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify console.error was called with the error message
|
||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
||||
expect(stripAnsi(String(consoleErrorSpy.mock.calls[0][0]))).toBe(
|
||||
'Error in /test/settings.json: Test settings error',
|
||||
);
|
||||
expect(stripAnsi(String(consoleErrorSpy.mock.calls[1][0]))).toBe(
|
||||
'Please fix /test/settings.json and try again.',
|
||||
);
|
||||
|
||||
// Verify process.exit was called (indirectly, via the thrown error)
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import App from './ui/App.js';
|
||||
import { parseArguments } from './config/args.js';
|
||||
import { loadEnvironment } from './config/env.js';
|
||||
import { getTargetDirectory } from './utils/paths.js';
|
||||
import { toolRegistry } from './tools/tool-registry.js';
|
||||
import { LSTool } from './tools/ls.tool.js';
|
||||
import { ReadFileTool } from './tools/read-file.tool.js';
|
||||
import { GrepTool } from './tools/grep.tool.js';
|
||||
import { GlobTool } from './tools/glob.tool.js';
|
||||
import { EditTool } from './tools/edit.tool.js';
|
||||
import { TerminalTool } from './tools/terminal.tool.js';
|
||||
import { WriteFileTool } from './tools/write-file.tool.js';
|
||||
|
||||
async function main() {
|
||||
// 1. Configuration
|
||||
loadEnvironment();
|
||||
const argv = await parseArguments(); // Ensure args.ts imports printWarning from ui/display
|
||||
const targetDir = getTargetDirectory(argv.target_dir);
|
||||
|
||||
// 2. Configure tools
|
||||
registerTools(targetDir);
|
||||
|
||||
// 3. Render UI
|
||||
render(React.createElement(App, { directory: targetDir }));
|
||||
}
|
||||
|
||||
// --- Global Unhandled Rejection Handler ---
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
// Check if this is the known 429 ClientError that sometimes escapes
|
||||
// this is a workaround for a specific issue with the way we are calling gemini
|
||||
// where a 429 error is thrown but not caught, causing an unhandled rejection
|
||||
// TODO(adh): Remove this when the race condition is fixed
|
||||
const isKnownEscaped429 =
|
||||
reason instanceof Error &&
|
||||
reason.name === 'ClientError' &&
|
||||
reason.message.includes('got status: 429');
|
||||
|
||||
if (isKnownEscaped429) {
|
||||
// Log it differently and DON'T exit, as it's likely already handled visually
|
||||
console.warn('-----------------------------------------');
|
||||
console.warn('WORKAROUND: Suppressed known escaped 429 Unhandled Rejection.');
|
||||
console.warn('-----------------------------------------');
|
||||
console.warn('Reason:', reason);
|
||||
// No process.exit(1);
|
||||
} else {
|
||||
// Log other unexpected unhandled rejections as critical errors
|
||||
console.error('=========================================');
|
||||
console.error('CRITICAL: Unhandled Promise Rejection!');
|
||||
console.error('=========================================');
|
||||
console.error('Reason:', reason);
|
||||
console.error('Stack trace may follow:');
|
||||
if (!(reason instanceof Error)) {
|
||||
console.error(reason);
|
||||
}
|
||||
// Exit for genuinely unhandled errors
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Global Entry Point ---
|
||||
main().catch((error) => {
|
||||
console.error('An unexpected critical error occurred:');
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
} else {
|
||||
console.error(String(error));
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function registerTools(targetDir: string) {
|
||||
const lsTool = new LSTool(targetDir);
|
||||
const readFileTool = new ReadFileTool(targetDir);
|
||||
const grepTool = new GrepTool(targetDir);
|
||||
const globTool = new GlobTool(targetDir);
|
||||
const editTool = new EditTool(targetDir);
|
||||
const terminalTool = new TerminalTool(targetDir);
|
||||
const writeFileTool = new WriteFileTool(targetDir);
|
||||
|
||||
toolRegistry.registerTool(lsTool);
|
||||
toolRegistry.registerTool(readFileTool);
|
||||
toolRegistry.registerTool(grepTool);
|
||||
toolRegistry.registerTool(globTool);
|
||||
toolRegistry.registerTool(editTool);
|
||||
toolRegistry.registerTool(terminalTool);
|
||||
toolRegistry.registerTool(writeFileTool);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { AppWrapper } from './ui/App.js';
|
||||
import { loadCliConfig } from './config/config.js';
|
||||
import { readStdin } from './utils/readStdin.js';
|
||||
import { basename } from 'node:path';
|
||||
import { start_sandbox } from './utils/sandbox.js';
|
||||
import {
|
||||
LoadedSettings,
|
||||
loadSettings,
|
||||
SettingScope,
|
||||
} from './config/settings.js';
|
||||
import { themeManager } from './ui/themes/theme-manager.js';
|
||||
import { getStartupWarnings } from './utils/startupWarnings.js';
|
||||
import { runNonInteractive } from './nonInteractiveCli.js';
|
||||
import { loadExtensions, Extension } from './config/extension.js';
|
||||
import { cleanupCheckpoints } from './utils/cleanup.js';
|
||||
import {
|
||||
ApprovalMode,
|
||||
Config,
|
||||
EditTool,
|
||||
ShellTool,
|
||||
WriteFileTool,
|
||||
sessionId,
|
||||
logUserPrompt,
|
||||
AuthType,
|
||||
} from '@gemini-cli/core';
|
||||
import { validateAuthMethod } from './config/auth.js';
|
||||
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
|
||||
|
||||
export async function main() {
|
||||
const workspaceRoot = process.cwd();
|
||||
const settings = loadSettings(workspaceRoot);
|
||||
|
||||
await cleanupCheckpoints();
|
||||
if (settings.errors.length > 0) {
|
||||
for (const error of settings.errors) {
|
||||
let errorMessage = `Error in ${error.path}: ${error.message}`;
|
||||
if (!process.env.NO_COLOR) {
|
||||
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
|
||||
}
|
||||
console.error(errorMessage);
|
||||
console.error(`Please fix ${error.path} and try again.`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const extensions = loadExtensions(workspaceRoot);
|
||||
const config = await loadCliConfig(settings.merged, extensions, sessionId);
|
||||
|
||||
// set default fallback to gemini api key
|
||||
// this has to go after load cli becuase thats where the env is set
|
||||
if (!settings.merged.selectedAuthType && process.env.GEMINI_API_KEY) {
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'selectedAuthType',
|
||||
AuthType.USE_GEMINI,
|
||||
);
|
||||
}
|
||||
|
||||
setMaxSizedBoxDebugging(config.getDebugMode());
|
||||
|
||||
// Initialize centralized FileDiscoveryService
|
||||
config.getFileService();
|
||||
if (config.getCheckpointingEnabled()) {
|
||||
try {
|
||||
await config.getGitService();
|
||||
} catch {
|
||||
// For now swallow the error, later log it.
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.merged.theme) {
|
||||
if (!themeManager.setActiveTheme(settings.merged.theme)) {
|
||||
// If the theme is not found during initial load, log a warning and continue.
|
||||
// The useThemeCommand hook in App.tsx will handle opening the dialog.
|
||||
console.warn(`Warning: Theme "${settings.merged.theme}" not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
// hop into sandbox if we are outside and sandboxing is enabled
|
||||
if (!process.env.SANDBOX) {
|
||||
const sandboxConfig = config.getSandbox();
|
||||
if (sandboxConfig) {
|
||||
if (settings.merged.selectedAuthType) {
|
||||
// Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.
|
||||
const err = validateAuthMethod(settings.merged.selectedAuthType);
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
await config.refreshAuth(settings.merged.selectedAuthType);
|
||||
}
|
||||
await start_sandbox(sandboxConfig);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
let input = config.getQuestion();
|
||||
const startupWarnings = await getStartupWarnings();
|
||||
|
||||
// Render UI, passing necessary config values. Check that there is no command line question.
|
||||
if (process.stdin.isTTY && input?.length === 0) {
|
||||
setWindowTitle(basename(workspaceRoot), settings);
|
||||
render(
|
||||
<React.StrictMode>
|
||||
<AppWrapper
|
||||
config={config}
|
||||
settings={settings}
|
||||
startupWarnings={startupWarnings}
|
||||
/>
|
||||
</React.StrictMode>,
|
||||
{ exitOnCtrlC: false },
|
||||
);
|
||||
return;
|
||||
}
|
||||
// If not a TTY, read from stdin
|
||||
// This is for cases where the user pipes input directly into the command
|
||||
if (!process.stdin.isTTY) {
|
||||
input += await readStdin();
|
||||
}
|
||||
if (!input) {
|
||||
console.error('No input provided via stdin.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logUserPrompt(config, {
|
||||
prompt: input,
|
||||
prompt_length: input.length,
|
||||
});
|
||||
|
||||
// Non-interactive mode handled by runNonInteractive
|
||||
const nonInteractiveConfig = await loadNonInteractiveConfig(
|
||||
config,
|
||||
extensions,
|
||||
settings,
|
||||
);
|
||||
|
||||
await runNonInteractive(nonInteractiveConfig, input);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function setWindowTitle(title: string, settings: LoadedSettings) {
|
||||
if (!settings.merged.hideWindowTitle) {
|
||||
process.stdout.write(`\x1b]2; Gemini - ${title} \x07`);
|
||||
|
||||
process.on('exit', () => {
|
||||
process.stdout.write(`\x1b]2;\x07`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Global Unhandled Rejection Handler ---
|
||||
process.on('unhandledRejection', (reason, _promise) => {
|
||||
// Log other unexpected unhandled rejections as critical errors
|
||||
console.error('=========================================');
|
||||
console.error('CRITICAL: Unhandled Promise Rejection!');
|
||||
console.error('=========================================');
|
||||
console.error('Reason:', reason);
|
||||
console.error('Stack trace may follow:');
|
||||
if (!(reason instanceof Error)) {
|
||||
console.error(reason);
|
||||
}
|
||||
// Exit for genuinely unhandled errors
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function loadNonInteractiveConfig(
|
||||
config: Config,
|
||||
extensions: Extension[],
|
||||
settings: LoadedSettings,
|
||||
) {
|
||||
let finalConfig = config;
|
||||
if (config.getApprovalMode() !== ApprovalMode.YOLO) {
|
||||
// Everything is not allowed, ensure that only read-only tools are configured.
|
||||
const existingExcludeTools = settings.merged.excludeTools || [];
|
||||
const interactiveTools = [
|
||||
ShellTool.Name,
|
||||
EditTool.Name,
|
||||
WriteFileTool.Name,
|
||||
];
|
||||
|
||||
const newExcludeTools = [
|
||||
...new Set([...existingExcludeTools, ...interactiveTools]),
|
||||
];
|
||||
|
||||
const nonInteractiveSettings = {
|
||||
...settings.merged,
|
||||
excludeTools: newExcludeTools,
|
||||
};
|
||||
finalConfig = await loadCliConfig(
|
||||
nonInteractiveSettings,
|
||||
extensions,
|
||||
config.getSessionId(),
|
||||
);
|
||||
}
|
||||
|
||||
return await validateNonInterActiveAuth(
|
||||
settings.merged.selectedAuthType,
|
||||
finalConfig,
|
||||
);
|
||||
}
|
||||
|
||||
async function validateNonInterActiveAuth(
|
||||
selectedAuthType: AuthType | undefined,
|
||||
nonInteractiveConfig: Config,
|
||||
) {
|
||||
// making a special case for the cli. many headless environments might not have a settings.json set
|
||||
// so if GEMINI_API_KEY is set, we'll use that. However since the oauth things are interactive anyway, we'll
|
||||
// still expect that exists
|
||||
if (!selectedAuthType && !process.env.GEMINI_API_KEY) {
|
||||
console.error(
|
||||
'Please set an Auth method in your .gemini/settings.json OR specify GEMINI_API_KEY env variable file before running',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
selectedAuthType = selectedAuthType || AuthType.USE_GEMINI;
|
||||
const err = validateAuthMethod(selectedAuthType);
|
||||
if (err != null) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await nonInteractiveConfig.refreshAuth(selectedAuthType);
|
||||
return nonInteractiveConfig;
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { runNonInteractive } from './nonInteractiveCli.js';
|
||||
import { Config, GeminiClient, ToolRegistry } from '@gemini-cli/core';
|
||||
import { GenerateContentResponse, Part, FunctionCall } from '@google/genai';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@gemini-cli/core', async () => {
|
||||
const actualCore =
|
||||
await vi.importActual<typeof import('@gemini-cli/core')>(
|
||||
'@gemini-cli/core',
|
||||
);
|
||||
return {
|
||||
...actualCore,
|
||||
GeminiClient: vi.fn(),
|
||||
ToolRegistry: vi.fn(),
|
||||
executeToolCall: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('runNonInteractive', () => {
|
||||
let mockConfig: Config;
|
||||
let mockGeminiClient: GeminiClient;
|
||||
let mockToolRegistry: ToolRegistry;
|
||||
let mockChat: {
|
||||
sendMessageStream: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockProcessStdoutWrite: ReturnType<typeof vi.fn>;
|
||||
let mockProcessExit: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockChat = {
|
||||
sendMessageStream: vi.fn(),
|
||||
};
|
||||
mockGeminiClient = {
|
||||
getChat: vi.fn().mockResolvedValue(mockChat),
|
||||
} as unknown as GeminiClient;
|
||||
mockToolRegistry = {
|
||||
getFunctionDeclarations: vi.fn().mockReturnValue([]),
|
||||
getTool: vi.fn(),
|
||||
} as unknown as ToolRegistry;
|
||||
|
||||
vi.mocked(GeminiClient).mockImplementation(() => mockGeminiClient);
|
||||
vi.mocked(ToolRegistry).mockImplementation(() => mockToolRegistry);
|
||||
|
||||
mockConfig = {
|
||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
|
||||
} as unknown as Config;
|
||||
|
||||
mockProcessStdoutWrite = vi.fn().mockImplementation(() => true);
|
||||
process.stdout.write = mockProcessStdoutWrite as any; // Use any to bypass strict signature matching for mock
|
||||
mockProcessExit = vi
|
||||
.fn()
|
||||
.mockImplementation((_code?: number) => undefined as never);
|
||||
process.exit = mockProcessExit as any; // Use any for process.exit mock
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
// Restore original process methods if they were globally patched
|
||||
// This might require storing the original methods before patching them in beforeEach
|
||||
});
|
||||
|
||||
it('should process input and write text output', async () => {
|
||||
const inputStream = (async function* () {
|
||||
yield {
|
||||
candidates: [{ content: { parts: [{ text: 'Hello' }] } }],
|
||||
} as GenerateContentResponse;
|
||||
yield {
|
||||
candidates: [{ content: { parts: [{ text: ' World' }] } }],
|
||||
} as GenerateContentResponse;
|
||||
})();
|
||||
mockChat.sendMessageStream.mockResolvedValue(inputStream);
|
||||
|
||||
await runNonInteractive(mockConfig, 'Test input');
|
||||
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalledWith({
|
||||
message: [{ text: 'Test input' }],
|
||||
config: {
|
||||
abortSignal: expect.any(AbortSignal),
|
||||
tools: [{ functionDeclarations: [] }],
|
||||
},
|
||||
});
|
||||
expect(mockProcessStdoutWrite).toHaveBeenCalledWith('Hello');
|
||||
expect(mockProcessStdoutWrite).toHaveBeenCalledWith(' World');
|
||||
expect(mockProcessStdoutWrite).toHaveBeenCalledWith('\n');
|
||||
});
|
||||
|
||||
it('should handle a single tool call and respond', async () => {
|
||||
const functionCall: FunctionCall = {
|
||||
id: 'fc1',
|
||||
name: 'testTool',
|
||||
args: { p: 'v' },
|
||||
};
|
||||
const toolResponsePart: Part = {
|
||||
functionResponse: {
|
||||
name: 'testTool',
|
||||
id: 'fc1',
|
||||
response: { result: 'tool success' },
|
||||
},
|
||||
};
|
||||
|
||||
const { executeToolCall: mockCoreExecuteToolCall } = await import(
|
||||
'@gemini-cli/core'
|
||||
);
|
||||
vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
|
||||
callId: 'fc1',
|
||||
responseParts: [toolResponsePart],
|
||||
resultDisplay: 'Tool success display',
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const stream1 = (async function* () {
|
||||
yield { functionCalls: [functionCall] } as GenerateContentResponse;
|
||||
})();
|
||||
const stream2 = (async function* () {
|
||||
yield {
|
||||
candidates: [{ content: { parts: [{ text: 'Final answer' }] } }],
|
||||
} as GenerateContentResponse;
|
||||
})();
|
||||
mockChat.sendMessageStream
|
||||
.mockResolvedValueOnce(stream1)
|
||||
.mockResolvedValueOnce(stream2);
|
||||
|
||||
await runNonInteractive(mockConfig, 'Use a tool');
|
||||
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(2);
|
||||
expect(mockCoreExecuteToolCall).toHaveBeenCalledWith(
|
||||
mockConfig,
|
||||
expect.objectContaining({ callId: 'fc1', name: 'testTool' }),
|
||||
mockToolRegistry,
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
expect(mockChat.sendMessageStream).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
message: [toolResponsePart],
|
||||
}),
|
||||
);
|
||||
expect(mockProcessStdoutWrite).toHaveBeenCalledWith('Final answer');
|
||||
});
|
||||
|
||||
it('should handle error during tool execution', async () => {
|
||||
const functionCall: FunctionCall = {
|
||||
id: 'fcError',
|
||||
name: 'errorTool',
|
||||
args: {},
|
||||
};
|
||||
const errorResponsePart: Part = {
|
||||
functionResponse: {
|
||||
name: 'errorTool',
|
||||
id: 'fcError',
|
||||
response: { error: 'Tool failed' },
|
||||
},
|
||||
};
|
||||
|
||||
const { executeToolCall: mockCoreExecuteToolCall } = await import(
|
||||
'@gemini-cli/core'
|
||||
);
|
||||
vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
|
||||
callId: 'fcError',
|
||||
responseParts: [errorResponsePart],
|
||||
resultDisplay: 'Tool execution failed badly',
|
||||
error: new Error('Tool failed'),
|
||||
});
|
||||
|
||||
const stream1 = (async function* () {
|
||||
yield { functionCalls: [functionCall] } as GenerateContentResponse;
|
||||
})();
|
||||
|
||||
const stream2 = (async function* () {
|
||||
yield {
|
||||
candidates: [
|
||||
{ content: { parts: [{ text: 'Could not complete request.' }] } },
|
||||
],
|
||||
} as GenerateContentResponse;
|
||||
})();
|
||||
mockChat.sendMessageStream
|
||||
.mockResolvedValueOnce(stream1)
|
||||
.mockResolvedValueOnce(stream2);
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await runNonInteractive(mockConfig, 'Trigger tool error');
|
||||
|
||||
expect(mockCoreExecuteToolCall).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool errorTool: Tool execution failed badly',
|
||||
);
|
||||
expect(mockChat.sendMessageStream).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
message: [errorResponsePart],
|
||||
}),
|
||||
);
|
||||
expect(mockProcessStdoutWrite).toHaveBeenCalledWith(
|
||||
'Could not complete request.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should exit with error if sendMessageStream throws initially', async () => {
|
||||
const apiError = new Error('API connection failed');
|
||||
mockChat.sendMessageStream.mockRejectedValue(apiError);
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await runNonInteractive(mockConfig, 'Initial fail');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error processing input:',
|
||||
apiError,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
Config,
|
||||
ToolCallRequestInfo,
|
||||
executeToolCall,
|
||||
ToolRegistry,
|
||||
shutdownTelemetry,
|
||||
isTelemetrySdkInitialized,
|
||||
} from '@gemini-cli/core';
|
||||
import {
|
||||
Content,
|
||||
Part,
|
||||
FunctionCall,
|
||||
GenerateContentResponse,
|
||||
} from '@google/genai';
|
||||
|
||||
function getResponseText(response: GenerateContentResponse): string | null {
|
||||
if (response.candidates && response.candidates.length > 0) {
|
||||
const candidate = response.candidates[0];
|
||||
if (
|
||||
candidate.content &&
|
||||
candidate.content.parts &&
|
||||
candidate.content.parts.length > 0
|
||||
) {
|
||||
// We are running in headless mode so we don't need to return thoughts to STDOUT.
|
||||
const thoughtPart = candidate.content.parts[0];
|
||||
if (thoughtPart?.thought) {
|
||||
return null;
|
||||
}
|
||||
return candidate.content.parts
|
||||
.filter((part) => part.text)
|
||||
.map((part) => part.text)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function runNonInteractive(
|
||||
config: Config,
|
||||
input: string,
|
||||
): Promise<void> {
|
||||
const geminiClient = config.getGeminiClient();
|
||||
const toolRegistry: ToolRegistry = await config.getToolRegistry();
|
||||
|
||||
const chat = await geminiClient.getChat();
|
||||
const abortController = new AbortController();
|
||||
let currentMessages: Content[] = [{ role: 'user', parts: [{ text: input }] }];
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const functionCalls: FunctionCall[] = [];
|
||||
|
||||
const responseStream = await chat.sendMessageStream({
|
||||
message: currentMessages[0]?.parts || [], // Ensure parts are always provided
|
||||
config: {
|
||||
abortSignal: abortController.signal,
|
||||
tools: [
|
||||
{ functionDeclarations: toolRegistry.getFunctionDeclarations() },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
for await (const resp of responseStream) {
|
||||
if (abortController.signal.aborted) {
|
||||
console.error('Operation cancelled.');
|
||||
return;
|
||||
}
|
||||
const textPart = getResponseText(resp);
|
||||
if (textPart) {
|
||||
process.stdout.write(textPart);
|
||||
}
|
||||
if (resp.functionCalls) {
|
||||
functionCalls.push(...resp.functionCalls);
|
||||
}
|
||||
}
|
||||
|
||||
if (functionCalls.length > 0) {
|
||||
const toolResponseParts: Part[] = [];
|
||||
|
||||
for (const fc of functionCalls) {
|
||||
const callId = fc.id ?? `${fc.name}-${Date.now()}`;
|
||||
const requestInfo: ToolCallRequestInfo = {
|
||||
callId,
|
||||
name: fc.name as string,
|
||||
args: (fc.args ?? {}) as Record<string, unknown>,
|
||||
};
|
||||
|
||||
const toolResponse = await executeToolCall(
|
||||
config,
|
||||
requestInfo,
|
||||
toolRegistry,
|
||||
abortController.signal,
|
||||
);
|
||||
|
||||
if (toolResponse.error) {
|
||||
console.error(
|
||||
`Error executing tool ${fc.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (toolResponse.responseParts) {
|
||||
const parts = Array.isArray(toolResponse.responseParts)
|
||||
? toolResponse.responseParts
|
||||
: [toolResponse.responseParts];
|
||||
for (const part of parts) {
|
||||
if (typeof part === 'string') {
|
||||
toolResponseParts.push({ text: part });
|
||||
} else if (part) {
|
||||
toolResponseParts.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
currentMessages = [{ role: 'user', parts: toolResponseParts }];
|
||||
} else {
|
||||
process.stdout.write('\n'); // Ensure a final newline
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing input:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (isTelemetrySdkInitialized()) {
|
||||
await shutdownTelemetry();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { FunctionDeclaration, Schema } from '@google/genai';
|
||||
import { ToolResult } from './ToolResult.js';
|
||||
import { Tool } from './Tool.js';
|
||||
import { ToolCallConfirmationDetails } from '../ui/types.js';
|
||||
|
||||
/**
|
||||
* Base implementation for tools with common functionality
|
||||
*/
|
||||
export abstract class BaseTool<TParams = unknown, TResult extends ToolResult = ToolResult> implements Tool<TParams, TResult> {
|
||||
/**
|
||||
* Creates a new instance of BaseTool
|
||||
* @param name Internal name of the tool (used for API calls)
|
||||
* @param displayName User-friendly display name of the tool
|
||||
* @param description Description of what the tool does
|
||||
* @param parameterSchema JSON Schema defining the parameters
|
||||
*/
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly displayName: string,
|
||||
public readonly description: string,
|
||||
public readonly parameterSchema: Record<string, unknown>
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Function declaration schema computed from name, description, and parameterSchema
|
||||
*/
|
||||
get schema(): FunctionDeclaration {
|
||||
return {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
parameters: this.parameterSchema as Schema
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the parameters for the tool
|
||||
* This is a placeholder implementation and should be overridden
|
||||
* @param params Parameters to validate
|
||||
* @returns An error message string if invalid, null otherwise
|
||||
*/
|
||||
invalidParams(params: TParams): string | null {
|
||||
// Implementation would typically use a JSON Schema validator
|
||||
// This is a placeholder that should be implemented by derived classes
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pre-execution description of the tool operation
|
||||
* Default implementation that should be overridden by derived classes
|
||||
* @param params Parameters for the tool execution
|
||||
* @returns A markdown string describing what the tool will do
|
||||
*/
|
||||
getDescription(params: TParams): string {
|
||||
return JSON.stringify(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the tool should prompt for confirmation before execution
|
||||
* @param params Parameters for the tool execution
|
||||
* @returns Whether or not execute should be confirmed by the user.
|
||||
*/
|
||||
shouldConfirmExecute(params: TParams): Promise<ToolCallConfirmationDetails | false> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method to execute the tool with the given parameters
|
||||
* Must be implemented by derived classes
|
||||
* @param params Parameters for the tool execution
|
||||
* @returns Result of the tool execution
|
||||
*/
|
||||
abstract execute(params: TParams): Promise<TResult>;
|
||||
}
|
||||