A safe, simple, extensible, and fast agent harness
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Plugin install (#24)

* Install default plugins from GH release on server start

* Add plugin install/update to TUI

authored by

Mason Stallmo and committed by
GitHub
9813d1e8 78c57a1d

+1082 -32
+562 -16
Cargo.lock
··· 170 170 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 171 171 172 172 [[package]] 173 + name = "aws-lc-rs" 174 + version = "1.16.3" 175 + source = "registry+https://github.com/rust-lang/crates.io-index" 176 + checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" 177 + dependencies = [ 178 + "aws-lc-sys", 179 + "zeroize", 180 + ] 181 + 182 + [[package]] 183 + name = "aws-lc-sys" 184 + version = "0.40.0" 185 + source = "registry+https://github.com/rust-lang/crates.io-index" 186 + checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" 187 + dependencies = [ 188 + "cc", 189 + "cmake", 190 + "dunce", 191 + "fs_extra", 192 + ] 193 + 194 + [[package]] 173 195 name = "axum" 174 196 version = "0.7.9" 175 197 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 340 362 checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" 341 363 dependencies = [ 342 364 "ambient-authority", 343 - "rand", 365 + "rand 0.8.5", 344 366 ] 345 367 346 368 [[package]] ··· 395 417 "libc", 396 418 "shlex", 397 419 ] 420 + 421 + [[package]] 422 + name = "cesu8" 423 + version = "1.1.0" 424 + source = "registry+https://github.com/rust-lang/crates.io-index" 425 + checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 398 426 399 427 [[package]] 400 428 name = "cfg-if" ··· 403 431 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 404 432 405 433 [[package]] 434 + name = "cfg_aliases" 435 + version = "0.2.1" 436 + source = "registry+https://github.com/rust-lang/crates.io-index" 437 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 438 + 439 + [[package]] 406 440 name = "chrono" 407 441 version = "0.4.44" 408 442 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 456 490 checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" 457 491 458 492 [[package]] 493 + name = "cmake" 494 + version = "0.1.58" 495 + source = "registry+https://github.com/rust-lang/crates.io-index" 496 + checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" 497 + dependencies = [ 498 + "cc", 499 + ] 500 + 501 + [[package]] 459 502 name = "cobs" 460 503 version = "0.3.0" 461 504 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 471 514 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 472 515 473 516 [[package]] 517 + name = "combine" 518 + version = "4.6.7" 519 + source = "registry+https://github.com/rust-lang/crates.io-index" 520 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 521 + dependencies = [ 522 + "bytes", 523 + "memchr", 524 + ] 525 + 526 + [[package]] 474 527 name = "compact_str" 475 528 version = "0.8.1" 476 529 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 498 551 version = "0.9.6" 499 552 source = "registry+https://github.com/rust-lang/crates.io-index" 500 553 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 554 + 555 + [[package]] 556 + name = "core-foundation" 557 + version = "0.10.1" 558 + source = "registry+https://github.com/rust-lang/crates.io-index" 559 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 560 + dependencies = [ 561 + "core-foundation-sys", 562 + "libc", 563 + ] 501 564 502 565 [[package]] 503 566 name = "core-foundation-sys" ··· 902 965 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 903 966 904 967 [[package]] 968 + name = "dunce" 969 + version = "1.0.5" 970 + source = "registry+https://github.com/rust-lang/crates.io-index" 971 + checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 972 + 973 + [[package]] 905 974 name = "ein" 906 975 version = "0.1.6" 907 976 dependencies = [ ··· 955 1024 "ein-agent", 956 1025 "ein-proto", 957 1026 "ein_plugin", 1027 + "flate2", 958 1028 "futures", 959 1029 "hyper", 1030 + "reqwest", 960 1031 "serde", 961 1032 "serde_json", 962 1033 "sqlx", 1034 + "tar", 963 1035 "tokio", 964 1036 "tokio-stream", 965 1037 "tonic", ··· 1279 1351 ] 1280 1352 1281 1353 [[package]] 1354 + name = "fs_extra" 1355 + version = "1.3.0" 1356 + source = "registry+https://github.com/rust-lang/crates.io-index" 1357 + checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 1358 + 1359 + [[package]] 1282 1360 name = "fsevent-sys" 1283 1361 version = "4.1.0" 1284 1362 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1432 1510 checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 1433 1511 dependencies = [ 1434 1512 "cfg-if", 1513 + "js-sys", 1435 1514 "libc", 1436 1515 "wasi", 1516 + "wasm-bindgen", 1437 1517 ] 1438 1518 1439 1519 [[package]] ··· 1443 1523 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 1444 1524 dependencies = [ 1445 1525 "cfg-if", 1526 + "js-sys", 1446 1527 "libc", 1447 1528 "r-efi 5.3.0", 1448 1529 "wasip2", 1530 + "wasm-bindgen", 1449 1531 ] 1450 1532 1451 1533 [[package]] ··· 1636 1718 ] 1637 1719 1638 1720 [[package]] 1721 + name = "hyper-rustls" 1722 + version = "0.27.9" 1723 + source = "registry+https://github.com/rust-lang/crates.io-index" 1724 + checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" 1725 + dependencies = [ 1726 + "http", 1727 + "hyper", 1728 + "hyper-util", 1729 + "rustls 0.23.39", 1730 + "tokio", 1731 + "tokio-rustls 0.26.4", 1732 + "tower-service", 1733 + ] 1734 + 1735 + [[package]] 1639 1736 name = "hyper-timeout" 1640 1737 version = "0.5.2" 1641 1738 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1654 1751 source = "registry+https://github.com/rust-lang/crates.io-index" 1655 1752 checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 1656 1753 dependencies = [ 1754 + "base64", 1657 1755 "bytes", 1658 1756 "futures-channel", 1659 1757 "futures-util", 1660 1758 "http", 1661 1759 "http-body", 1662 1760 "hyper", 1761 + "ipnet", 1663 1762 "libc", 1763 + "percent-encoding", 1664 1764 "pin-project-lite", 1665 1765 "socket2 0.6.2", 1666 1766 "tokio", ··· 1813 1913 checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" 1814 1914 dependencies = [ 1815 1915 "bitmaps", 1816 - "rand_core", 1916 + "rand_core 0.6.4", 1817 1917 "rand_xoshiro", 1818 1918 "sized-chunks", 1819 1919 "typenum", ··· 1916 2016 checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" 1917 2017 1918 2018 [[package]] 2019 + name = "iri-string" 2020 + version = "0.7.12" 2021 + source = "registry+https://github.com/rust-lang/crates.io-index" 2022 + checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" 2023 + dependencies = [ 2024 + "memchr", 2025 + "serde", 2026 + ] 2027 + 2028 + [[package]] 1919 2029 name = "is_terminal_polyfill" 1920 2030 version = "1.70.2" 1921 2031 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1966 2076 ] 1967 2077 1968 2078 [[package]] 2079 + name = "jni" 2080 + version = "0.21.1" 2081 + source = "registry+https://github.com/rust-lang/crates.io-index" 2082 + checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 2083 + dependencies = [ 2084 + "cesu8", 2085 + "cfg-if", 2086 + "combine", 2087 + "jni-sys 0.3.1", 2088 + "log", 2089 + "thiserror 1.0.69", 2090 + "walkdir", 2091 + "windows-sys 0.45.0", 2092 + ] 2093 + 2094 + [[package]] 2095 + name = "jni-sys" 2096 + version = "0.3.1" 2097 + source = "registry+https://github.com/rust-lang/crates.io-index" 2098 + checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" 2099 + dependencies = [ 2100 + "jni-sys 0.4.1", 2101 + ] 2102 + 2103 + [[package]] 2104 + name = "jni-sys" 2105 + version = "0.4.1" 2106 + source = "registry+https://github.com/rust-lang/crates.io-index" 2107 + checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" 2108 + dependencies = [ 2109 + "jni-sys-macros", 2110 + ] 2111 + 2112 + [[package]] 2113 + name = "jni-sys-macros" 2114 + version = "0.4.1" 2115 + source = "registry+https://github.com/rust-lang/crates.io-index" 2116 + checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" 2117 + dependencies = [ 2118 + "quote", 2119 + "syn", 2120 + ] 2121 + 2122 + [[package]] 1969 2123 name = "jobserver" 1970 2124 version = "0.1.34" 1971 2125 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2110 2264 ] 2111 2265 2112 2266 [[package]] 2267 + name = "lru-slab" 2268 + version = "0.1.2" 2269 + source = "registry+https://github.com/rust-lang/crates.io-index" 2270 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2271 + 2272 + [[package]] 2113 2273 name = "mach2" 2114 2274 version = "0.4.3" 2115 2275 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2240 2400 "num-integer", 2241 2401 "num-iter", 2242 2402 "num-traits", 2243 - "rand", 2403 + "rand 0.8.5", 2244 2404 "smallvec", 2245 2405 "zeroize", 2246 2406 ] ··· 2326 2486 "cc", 2327 2487 "pkg-config", 2328 2488 ] 2489 + 2490 + [[package]] 2491 + name = "openssl-probe" 2492 + version = "0.2.1" 2493 + source = "registry+https://github.com/rust-lang/crates.io-index" 2494 + checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" 2329 2495 2330 2496 [[package]] 2331 2497 name = "option-ext" ··· 2611 2777 ] 2612 2778 2613 2779 [[package]] 2780 + name = "quinn" 2781 + version = "0.11.9" 2782 + source = "registry+https://github.com/rust-lang/crates.io-index" 2783 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 2784 + dependencies = [ 2785 + "bytes", 2786 + "cfg_aliases", 2787 + "pin-project-lite", 2788 + "quinn-proto", 2789 + "quinn-udp", 2790 + "rustc-hash", 2791 + "rustls 0.23.39", 2792 + "socket2 0.6.2", 2793 + "thiserror 2.0.18", 2794 + "tokio", 2795 + "tracing", 2796 + "web-time", 2797 + ] 2798 + 2799 + [[package]] 2800 + name = "quinn-proto" 2801 + version = "0.11.14" 2802 + source = "registry+https://github.com/rust-lang/crates.io-index" 2803 + checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" 2804 + dependencies = [ 2805 + "aws-lc-rs", 2806 + "bytes", 2807 + "getrandom 0.3.4", 2808 + "lru-slab", 2809 + "rand 0.9.4", 2810 + "ring", 2811 + "rustc-hash", 2812 + "rustls 0.23.39", 2813 + "rustls-pki-types", 2814 + "slab", 2815 + "thiserror 2.0.18", 2816 + "tinyvec", 2817 + "tracing", 2818 + "web-time", 2819 + ] 2820 + 2821 + [[package]] 2822 + name = "quinn-udp" 2823 + version = "0.5.14" 2824 + source = "registry+https://github.com/rust-lang/crates.io-index" 2825 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 2826 + dependencies = [ 2827 + "cfg_aliases", 2828 + "libc", 2829 + "once_cell", 2830 + "socket2 0.6.2", 2831 + "tracing", 2832 + "windows-sys 0.60.2", 2833 + ] 2834 + 2835 + [[package]] 2614 2836 name = "quote" 2615 2837 version = "1.0.45" 2616 2838 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2638 2860 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 2639 2861 dependencies = [ 2640 2862 "libc", 2641 - "rand_chacha", 2642 - "rand_core", 2863 + "rand_chacha 0.3.1", 2864 + "rand_core 0.6.4", 2865 + ] 2866 + 2867 + [[package]] 2868 + name = "rand" 2869 + version = "0.9.4" 2870 + source = "registry+https://github.com/rust-lang/crates.io-index" 2871 + checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" 2872 + dependencies = [ 2873 + "rand_chacha 0.9.0", 2874 + "rand_core 0.9.5", 2643 2875 ] 2644 2876 2645 2877 [[package]] ··· 2649 2881 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 2650 2882 dependencies = [ 2651 2883 "ppv-lite86", 2652 - "rand_core", 2884 + "rand_core 0.6.4", 2885 + ] 2886 + 2887 + [[package]] 2888 + name = "rand_chacha" 2889 + version = "0.9.0" 2890 + source = "registry+https://github.com/rust-lang/crates.io-index" 2891 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2892 + dependencies = [ 2893 + "ppv-lite86", 2894 + "rand_core 0.9.5", 2653 2895 ] 2654 2896 2655 2897 [[package]] ··· 2662 2904 ] 2663 2905 2664 2906 [[package]] 2907 + name = "rand_core" 2908 + version = "0.9.5" 2909 + source = "registry+https://github.com/rust-lang/crates.io-index" 2910 + checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" 2911 + dependencies = [ 2912 + "getrandom 0.3.4", 2913 + ] 2914 + 2915 + [[package]] 2665 2916 name = "rand_xoshiro" 2666 2917 version = "0.6.0" 2667 2918 source = "registry+https://github.com/rust-lang/crates.io-index" 2668 2919 checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" 2669 2920 dependencies = [ 2670 - "rand_core", 2921 + "rand_core 0.6.4", 2671 2922 ] 2672 2923 2673 2924 [[package]] ··· 2795 3046 checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 2796 3047 2797 3048 [[package]] 3049 + name = "reqwest" 3050 + version = "0.13.2" 3051 + source = "registry+https://github.com/rust-lang/crates.io-index" 3052 + checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" 3053 + dependencies = [ 3054 + "base64", 3055 + "bytes", 3056 + "futures-core", 3057 + "http", 3058 + "http-body", 3059 + "http-body-util", 3060 + "hyper", 3061 + "hyper-rustls", 3062 + "hyper-util", 3063 + "js-sys", 3064 + "log", 3065 + "percent-encoding", 3066 + "pin-project-lite", 3067 + "quinn", 3068 + "rustls 0.23.39", 3069 + "rustls-pki-types", 3070 + "rustls-platform-verifier", 3071 + "sync_wrapper", 3072 + "tokio", 3073 + "tokio-rustls 0.26.4", 3074 + "tower 0.5.3", 3075 + "tower-http", 3076 + "tower-service", 3077 + "url", 3078 + "wasm-bindgen", 3079 + "wasm-bindgen-futures", 3080 + "web-sys", 3081 + ] 3082 + 3083 + [[package]] 2798 3084 name = "ring" 2799 3085 version = "0.17.14" 2800 3086 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2821 3107 "num-traits", 2822 3108 "pkcs1", 2823 3109 "pkcs8", 2824 - "rand_core", 3110 + "rand_core 0.6.4", 2825 3111 "signature", 2826 3112 "spki", 2827 3113 "subtle", ··· 2885 3171 "log", 2886 3172 "ring", 2887 3173 "rustls-pki-types", 2888 - "rustls-webpki", 3174 + "rustls-webpki 0.102.8", 3175 + "subtle", 3176 + "zeroize", 3177 + ] 3178 + 3179 + [[package]] 3180 + name = "rustls" 3181 + version = "0.23.39" 3182 + source = "registry+https://github.com/rust-lang/crates.io-index" 3183 + checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" 3184 + dependencies = [ 3185 + "aws-lc-rs", 3186 + "once_cell", 3187 + "rustls-pki-types", 3188 + "rustls-webpki 0.103.13", 2889 3189 "subtle", 2890 3190 "zeroize", 2891 3191 ] 2892 3192 2893 3193 [[package]] 3194 + name = "rustls-native-certs" 3195 + version = "0.8.3" 3196 + source = "registry+https://github.com/rust-lang/crates.io-index" 3197 + checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" 3198 + dependencies = [ 3199 + "openssl-probe", 3200 + "rustls-pki-types", 3201 + "schannel", 3202 + "security-framework", 3203 + ] 3204 + 3205 + [[package]] 2894 3206 name = "rustls-pki-types" 2895 3207 version = "1.14.0" 2896 3208 source = "registry+https://github.com/rust-lang/crates.io-index" 2897 3209 checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" 2898 3210 dependencies = [ 3211 + "web-time", 2899 3212 "zeroize", 2900 3213 ] 2901 3214 2902 3215 [[package]] 3216 + name = "rustls-platform-verifier" 3217 + version = "0.6.2" 3218 + source = "registry+https://github.com/rust-lang/crates.io-index" 3219 + checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" 3220 + dependencies = [ 3221 + "core-foundation", 3222 + "core-foundation-sys", 3223 + "jni", 3224 + "log", 3225 + "once_cell", 3226 + "rustls 0.23.39", 3227 + "rustls-native-certs", 3228 + "rustls-platform-verifier-android", 3229 + "rustls-webpki 0.103.13", 3230 + "security-framework", 3231 + "security-framework-sys", 3232 + "webpki-root-certs", 3233 + "windows-sys 0.61.2", 3234 + ] 3235 + 3236 + [[package]] 3237 + name = "rustls-platform-verifier-android" 3238 + version = "0.1.1" 3239 + source = "registry+https://github.com/rust-lang/crates.io-index" 3240 + checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" 3241 + 3242 + [[package]] 2903 3243 name = "rustls-webpki" 2904 3244 version = "0.102.8" 2905 3245 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2911 3251 ] 2912 3252 2913 3253 [[package]] 3254 + name = "rustls-webpki" 3255 + version = "0.103.13" 3256 + source = "registry+https://github.com/rust-lang/crates.io-index" 3257 + checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" 3258 + dependencies = [ 3259 + "aws-lc-rs", 3260 + "ring", 3261 + "rustls-pki-types", 3262 + "untrusted", 3263 + ] 3264 + 3265 + [[package]] 2914 3266 name = "rustversion" 2915 3267 version = "1.0.22" 2916 3268 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2932 3284 ] 2933 3285 2934 3286 [[package]] 3287 + name = "schannel" 3288 + version = "0.1.29" 3289 + source = "registry+https://github.com/rust-lang/crates.io-index" 3290 + checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" 3291 + dependencies = [ 3292 + "windows-sys 0.61.2", 3293 + ] 3294 + 3295 + [[package]] 2935 3296 name = "scopeguard" 2936 3297 version = "1.2.0" 2937 3298 source = "registry+https://github.com/rust-lang/crates.io-index" 2938 3299 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2939 3300 2940 3301 [[package]] 3302 + name = "security-framework" 3303 + version = "3.7.0" 3304 + source = "registry+https://github.com/rust-lang/crates.io-index" 3305 + checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" 3306 + dependencies = [ 3307 + "bitflags 2.11.0", 3308 + "core-foundation", 3309 + "core-foundation-sys", 3310 + "libc", 3311 + "security-framework-sys", 3312 + ] 3313 + 3314 + [[package]] 3315 + name = "security-framework-sys" 3316 + version = "2.17.0" 3317 + source = "registry+https://github.com/rust-lang/crates.io-index" 3318 + checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" 3319 + dependencies = [ 3320 + "core-foundation-sys", 3321 + "libc", 3322 + ] 3323 + 3324 + [[package]] 2941 3325 name = "semver" 2942 3326 version = "1.0.27" 2943 3327 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3099 3483 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 3100 3484 dependencies = [ 3101 3485 "digest", 3102 - "rand_core", 3486 + "rand_core 0.6.4", 3103 3487 ] 3104 3488 3105 3489 [[package]] ··· 3286 3670 "memchr", 3287 3671 "once_cell", 3288 3672 "percent-encoding", 3289 - "rand", 3673 + "rand 0.8.5", 3290 3674 "rsa", 3291 3675 "serde", 3292 3676 "sha1", ··· 3324 3708 "md-5", 3325 3709 "memchr", 3326 3710 "once_cell", 3327 - "rand", 3711 + "rand 0.8.5", 3328 3712 "serde", 3329 3713 "serde_json", 3330 3714 "sha2", ··· 3433 3817 version = "1.0.2" 3434 3818 source = "registry+https://github.com/rust-lang/crates.io-index" 3435 3819 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 3820 + dependencies = [ 3821 + "futures-core", 3822 + ] 3436 3823 3437 3824 [[package]] 3438 3825 name = "synstructure" ··· 3483 3870 ] 3484 3871 3485 3872 [[package]] 3873 + name = "tar" 3874 + version = "0.4.45" 3875 + source = "registry+https://github.com/rust-lang/crates.io-index" 3876 + checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" 3877 + dependencies = [ 3878 + "filetime", 3879 + "libc", 3880 + "xattr", 3881 + ] 3882 + 3883 + [[package]] 3486 3884 name = "target-lexicon" 3487 3885 version = "0.13.5" 3488 3886 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3648 4046 source = "registry+https://github.com/rust-lang/crates.io-index" 3649 4047 checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" 3650 4048 dependencies = [ 3651 - "rustls", 4049 + "rustls 0.22.4", 3652 4050 "rustls-pki-types", 4051 + "tokio", 4052 + ] 4053 + 4054 + [[package]] 4055 + name = "tokio-rustls" 4056 + version = "0.26.4" 4057 + source = "registry+https://github.com/rust-lang/crates.io-index" 4058 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 4059 + dependencies = [ 4060 + "rustls 0.23.39", 3653 4061 "tokio", 3654 4062 ] 3655 4063 ··· 3771 4179 "indexmap 1.9.3", 3772 4180 "pin-project", 3773 4181 "pin-project-lite", 3774 - "rand", 4182 + "rand 0.8.5", 3775 4183 "slab", 3776 4184 "tokio", 3777 4185 "tokio-util", ··· 3790 4198 "futures-util", 3791 4199 "pin-project-lite", 3792 4200 "sync_wrapper", 4201 + "tokio", 4202 + "tower-layer", 4203 + "tower-service", 4204 + ] 4205 + 4206 + [[package]] 4207 + name = "tower-http" 4208 + version = "0.6.8" 4209 + source = "registry+https://github.com/rust-lang/crates.io-index" 4210 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 4211 + dependencies = [ 4212 + "bitflags 2.11.0", 4213 + "bytes", 4214 + "futures-util", 4215 + "http", 4216 + "http-body", 4217 + "iri-string", 4218 + "pin-project-lite", 4219 + "tower 0.5.3", 3793 4220 "tower-layer", 3794 4221 "tower-service", 3795 4222 ] ··· 4081 4508 "rustversion", 4082 4509 "wasm-bindgen-macro", 4083 4510 "wasm-bindgen-shared", 4511 + ] 4512 + 4513 + [[package]] 4514 + name = "wasm-bindgen-futures" 4515 + version = "0.4.64" 4516 + source = "registry+https://github.com/rust-lang/crates.io-index" 4517 + checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" 4518 + dependencies = [ 4519 + "cfg-if", 4520 + "futures-util", 4521 + "js-sys", 4522 + "once_cell", 4523 + "wasm-bindgen", 4524 + "web-sys", 4084 4525 ] 4085 4526 4086 4527 [[package]] ··· 4512 4953 "http-body", 4513 4954 "http-body-util", 4514 4955 "hyper", 4515 - "rustls", 4956 + "rustls 0.22.4", 4516 4957 "tokio", 4517 - "tokio-rustls", 4958 + "tokio-rustls 0.25.0", 4518 4959 "tracing", 4519 4960 "wasmtime", 4520 4961 "wasmtime-wasi", ··· 4564 5005 checksum = "cd48d1679b6858988cb96b154dda0ec5bbb09275b71db46057be37332d5477be" 4565 5006 dependencies = [ 4566 5007 "wast 245.0.1", 5008 + ] 5009 + 5010 + [[package]] 5011 + name = "web-sys" 5012 + version = "0.3.91" 5013 + source = "registry+https://github.com/rust-lang/crates.io-index" 5014 + checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" 5015 + dependencies = [ 5016 + "js-sys", 5017 + "wasm-bindgen", 5018 + ] 5019 + 5020 + [[package]] 5021 + name = "web-time" 5022 + version = "1.1.0" 5023 + source = "registry+https://github.com/rust-lang/crates.io-index" 5024 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 5025 + dependencies = [ 5026 + "js-sys", 5027 + "wasm-bindgen", 5028 + ] 5029 + 5030 + [[package]] 5031 + name = "webpki-root-certs" 5032 + version = "1.0.7" 5033 + source = "registry+https://github.com/rust-lang/crates.io-index" 5034 + checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" 5035 + dependencies = [ 5036 + "rustls-pki-types", 4567 5037 ] 4568 5038 4569 5039 [[package]] ··· 4745 5215 4746 5216 [[package]] 4747 5217 name = "windows-sys" 5218 + version = "0.45.0" 5219 + source = "registry+https://github.com/rust-lang/crates.io-index" 5220 + checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 5221 + dependencies = [ 5222 + "windows-targets 0.42.2", 5223 + ] 5224 + 5225 + [[package]] 5226 + name = "windows-sys" 4748 5227 version = "0.48.0" 4749 5228 source = "registry+https://github.com/rust-lang/crates.io-index" 4750 5229 checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" ··· 4781 5260 4782 5261 [[package]] 4783 5262 name = "windows-targets" 5263 + version = "0.42.2" 5264 + source = "registry+https://github.com/rust-lang/crates.io-index" 5265 + checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 5266 + dependencies = [ 5267 + "windows_aarch64_gnullvm 0.42.2", 5268 + "windows_aarch64_msvc 0.42.2", 5269 + "windows_i686_gnu 0.42.2", 5270 + "windows_i686_msvc 0.42.2", 5271 + "windows_x86_64_gnu 0.42.2", 5272 + "windows_x86_64_gnullvm 0.42.2", 5273 + "windows_x86_64_msvc 0.42.2", 5274 + ] 5275 + 5276 + [[package]] 5277 + name = "windows-targets" 4784 5278 version = "0.48.5" 4785 5279 source = "registry+https://github.com/rust-lang/crates.io-index" 4786 5280 checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" ··· 4829 5323 4830 5324 [[package]] 4831 5325 name = "windows_aarch64_gnullvm" 5326 + version = "0.42.2" 5327 + source = "registry+https://github.com/rust-lang/crates.io-index" 5328 + checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 5329 + 5330 + [[package]] 5331 + name = "windows_aarch64_gnullvm" 4832 5332 version = "0.48.5" 4833 5333 source = "registry+https://github.com/rust-lang/crates.io-index" 4834 5334 checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" ··· 4847 5347 4848 5348 [[package]] 4849 5349 name = "windows_aarch64_msvc" 5350 + version = "0.42.2" 5351 + source = "registry+https://github.com/rust-lang/crates.io-index" 5352 + checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 5353 + 5354 + [[package]] 5355 + name = "windows_aarch64_msvc" 4850 5356 version = "0.48.5" 4851 5357 source = "registry+https://github.com/rust-lang/crates.io-index" 4852 5358 checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" ··· 4865 5371 4866 5372 [[package]] 4867 5373 name = "windows_i686_gnu" 5374 + version = "0.42.2" 5375 + source = "registry+https://github.com/rust-lang/crates.io-index" 5376 + checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 5377 + 5378 + [[package]] 5379 + name = "windows_i686_gnu" 4868 5380 version = "0.48.5" 4869 5381 source = "registry+https://github.com/rust-lang/crates.io-index" 4870 5382 checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" ··· 4895 5407 4896 5408 [[package]] 4897 5409 name = "windows_i686_msvc" 5410 + version = "0.42.2" 5411 + source = "registry+https://github.com/rust-lang/crates.io-index" 5412 + checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 5413 + 5414 + [[package]] 5415 + name = "windows_i686_msvc" 4898 5416 version = "0.48.5" 4899 5417 source = "registry+https://github.com/rust-lang/crates.io-index" 4900 5418 checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" ··· 4913 5431 4914 5432 [[package]] 4915 5433 name = "windows_x86_64_gnu" 5434 + version = "0.42.2" 5435 + source = "registry+https://github.com/rust-lang/crates.io-index" 5436 + checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 5437 + 5438 + [[package]] 5439 + name = "windows_x86_64_gnu" 4916 5440 version = "0.48.5" 4917 5441 source = "registry+https://github.com/rust-lang/crates.io-index" 4918 5442 checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" ··· 4928 5452 version = "0.53.1" 4929 5453 source = "registry+https://github.com/rust-lang/crates.io-index" 4930 5454 checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 5455 + 5456 + [[package]] 5457 + name = "windows_x86_64_gnullvm" 5458 + version = "0.42.2" 5459 + source = "registry+https://github.com/rust-lang/crates.io-index" 5460 + checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 4931 5461 4932 5462 [[package]] 4933 5463 name = "windows_x86_64_gnullvm" ··· 4946 5476 version = "0.53.1" 4947 5477 source = "registry+https://github.com/rust-lang/crates.io-index" 4948 5478 checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 5479 + 5480 + [[package]] 5481 + name = "windows_x86_64_msvc" 5482 + version = "0.42.2" 5483 + source = "registry+https://github.com/rust-lang/crates.io-index" 5484 + checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 4949 5485 4950 5486 [[package]] 4951 5487 name = "windows_x86_64_msvc" ··· 5208 5744 dependencies = [ 5209 5745 "quote", 5210 5746 "syn", 5747 + ] 5748 + 5749 + [[package]] 5750 + name = "xattr" 5751 + version = "1.6.1" 5752 + source = "registry+https://github.com/rust-lang/crates.io-index" 5753 + checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" 5754 + dependencies = [ 5755 + "libc", 5756 + "rustix 1.1.4", 5211 5757 ] 5212 5758 5213 5759 [[package]]
+1 -2
crates/ein-agent/src/agents.rs
··· 185 185 return Ok(String::new()); 186 186 } 187 187 188 - const COMPACT_PROMPT: &str = 189 - "Please provide a detailed but concise summary of our conversation so far. \ 188 + const COMPACT_PROMPT: &str = "Please provide a detailed but concise summary of our conversation so far. \ 190 189 Include: goals discussed, files viewed or modified, code written or changed, \ 191 190 decisions made, and the current state of any ongoing tasks. \ 192 191 This summary will replace the full conversation history as context for \
+27
crates/ein-proto/proto/ein.proto
··· 12 12 rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse); 13 13 // Permanently deletes a session and its message history. 14 14 rpc DeleteSession(DeleteSessionRequest) returns (DeleteSessionResponse); 15 + // Returns the list of plugin sources with their installation status. 16 + rpc CheckPlugins(CheckPluginsRequest) returns (CheckPluginsResponse); 17 + // Downloads and installs plugins for the given source. 18 + rpc InstallPlugins(InstallPluginsRequest) returns (InstallPluginsResponse); 15 19 } 16 20 17 21 // A single message sent by the client during a session. ··· 160 164 } 161 165 162 166 message DeleteSessionResponse {} 167 + 168 + message CheckPluginsRequest {} 169 + 170 + // The installation status of a single plugin source. 171 + message PluginSourceStatus { 172 + string id = 1; 173 + string display_name = 2; 174 + bool installed = 3; 175 + } 176 + 177 + message CheckPluginsResponse { 178 + repeated PluginSourceStatus sources = 1; 179 + } 180 + 181 + message InstallPluginsRequest { 182 + // Identifier for the plugin source to install (e.g. "default"). 183 + string source_id = 1; 184 + } 185 + 186 + message InstallPluginsResponse { 187 + bool success = 1; 188 + string message = 2; 189 + }
+3
crates/ein-server/Cargo.toml
··· 32 32 wasmtime = "42.0.1" 33 33 wasmtime-wasi = "42.0.1" 34 34 wasmtime-wasi-http = "42.0.1" 35 + reqwest = { version = "0.13.2", default-features = false, features = ["rustls"] } 36 + flate2 = "1.1.9" 37 + tar = "0.4.45"
+45 -4
crates/ein-server/src/grpc.rs
··· 34 34 use crate::model_client::ModelClientSessionManager; 35 35 use crate::tools::ToolSetManager; 36 36 use ein_proto::ein::{ 37 - AgentError, AgentEvent as AgentEventProto, DeleteSessionRequest, DeleteSessionResponse, 38 - HistoryMessage, HistoryToolCall, ListSessionsRequest, ListSessionsResponse, SessionStarted, 39 - SessionSummary, UserInput, agent_event::Event, agent_server::Agent as AgentService, 40 - user_input, 37 + AgentError, AgentEvent as AgentEventProto, CheckPluginsRequest, CheckPluginsResponse, 38 + DeleteSessionRequest, DeleteSessionResponse, HistoryMessage, HistoryToolCall, 39 + InstallPluginsRequest, InstallPluginsResponse, ListSessionsRequest, ListSessionsResponse, 40 + PluginSourceStatus, SessionStarted, SessionSummary, UserInput, agent_event::Event, 41 + agent_server::Agent as AgentService, user_input, 41 42 }; 42 43 43 44 /// gRPC service struct. ··· 117 118 118 119 println!("[session] deleted session {id}"); 119 120 Ok(Response::new(DeleteSessionResponse {})) 121 + } 122 + 123 + async fn check_plugins( 124 + &self, 125 + _request: Request<CheckPluginsRequest>, 126 + ) -> Result<Response<CheckPluginsResponse>, Status> { 127 + let installed = crate::plugins::check_default_plugins( 128 + &self.config.plugin_dir, 129 + &self.config.model_client_dir, 130 + ) 131 + .await; 132 + Ok(Response::new(CheckPluginsResponse { 133 + sources: vec![PluginSourceStatus { 134 + id: "default".to_string(), 135 + display_name: "Default plugins".to_string(), 136 + installed, 137 + }], 138 + })) 139 + } 140 + 141 + async fn install_plugins( 142 + &self, 143 + request: Request<InstallPluginsRequest>, 144 + ) -> Result<Response<InstallPluginsResponse>, Status> { 145 + let source_id = request.into_inner().source_id; 146 + match source_id.as_str() { 147 + "default" => match crate::plugins::install_plugins(None).await { 148 + Ok(()) => Ok(Response::new(InstallPluginsResponse { 149 + success: true, 150 + message: "Default plugins installed successfully".to_string(), 151 + })), 152 + Err(e) => Ok(Response::new(InstallPluginsResponse { 153 + success: false, 154 + message: e.to_string(), 155 + })), 156 + }, 157 + _ => Err(Status::invalid_argument(format!( 158 + "Unknown plugin source: {source_id}" 159 + ))), 160 + } 120 161 } 121 162 122 163 /// Handles one client session.
+13
crates/ein-server/src/lib.rs
··· 9 9 mod grpc; 10 10 mod model_client; 11 11 mod persistence; 12 + mod plugins; 12 13 mod tools; 14 + 15 + pub use plugins::install_plugins; 13 16 14 17 use ein_proto::ein::agent_server::AgentServer as AgentServiceServer; 15 18 use grpc::AgentServer; ··· 55 58 56 59 /// Start the Ein gRPC server and block until it exits. 57 60 pub async fn run(port: u16) -> anyhow::Result<()> { 61 + // In release builds, auto-install plugins if none are present. 62 + if !cfg!(debug_assertions) { 63 + let config = EinConfig::default(); 64 + 65 + if plugins::plugins_missing(&config.plugin_dir).await { 66 + println!("No plugins found, downloading from GitHub release..."); 67 + plugins::install_plugins(None).await?; 68 + } 69 + } 70 + 58 71 let addr = format!("0.0.0.0:{port}").parse()?; 59 72 60 73 let server = AgentServer::new().await?;
+90
crates/ein-server/src/plugins.rs
··· 1 + // SPDX-License-Identifier: Apache-2.0 2 + // Copyright 2026 Mason Stallmo 3 + 4 + use anyhow::{Context, Result}; 5 + use flate2::read::GzDecoder; 6 + use std::{io, path::Path}; 7 + use tar::Archive; 8 + use tokio::{fs, task}; 9 + 10 + const DEFAULT_TOOL_PLUGINS: &[&str] = &["ein_bash", "ein_read", "ein_write", "ein_edit"]; 11 + const DEFAULT_MODEL_CLIENT_PLUGINS: &[&str] = &[ 12 + "ein_openrouter", 13 + "ein_anthropic", 14 + "ein_openai", 15 + "ein_ollama", 16 + ]; 17 + 18 + pub async fn install_plugins(version: Option<String>) -> Result<()> { 19 + let ver = version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); 20 + let ver = ver.trim_start_matches('v'); 21 + let tag = format!("v{ver}"); 22 + let url = 23 + format!("https://github.com/mstallmo/ein/releases/download/{tag}/ein-plugins-{tag}.tar.gz"); 24 + 25 + let plugins_dir = dirs::home_dir() 26 + .context("Failed to find home directory")? 27 + .join(".ein") 28 + .join("plugins"); 29 + 30 + fs::create_dir_all(plugins_dir.join("tools")).await?; 31 + fs::create_dir_all(plugins_dir.join("model_clients")).await?; 32 + 33 + println!("Downloading plugins from {url}..."); 34 + 35 + let response = reqwest::get(&url) 36 + .await 37 + .with_context(|| format!("Failed to download {url}"))?; 38 + 39 + if !response.status().is_success() { 40 + anyhow::bail!("Download failed: HTTP {}", response.status()); 41 + } 42 + 43 + let bytes = response 44 + .bytes() 45 + .await 46 + .context("Failed to read response body")?; 47 + 48 + task::spawn_blocking(move || { 49 + let gz = GzDecoder::new(io::Cursor::new(bytes)); 50 + Archive::new(gz) 51 + .unpack(&plugins_dir) 52 + .context("Failed to extract plugin archive") 53 + }) 54 + .await??; 55 + 56 + println!("Plugins installed successfully"); 57 + Ok(()) 58 + } 59 + 60 + /// Returns `true` if all default tool and model-client plugins are present. 61 + pub async fn check_default_plugins(plugin_dir: &Path, model_client_dir: &Path) -> bool { 62 + for name in DEFAULT_TOOL_PLUGINS { 63 + if !plugin_dir.join(format!("{name}.wasm")).exists() { 64 + return false; 65 + } 66 + } 67 + 68 + for name in DEFAULT_MODEL_CLIENT_PLUGINS { 69 + if !model_client_dir.join(format!("{name}.wasm")).exists() { 70 + return false; 71 + } 72 + } 73 + 74 + true 75 + } 76 + 77 + /// Returns true if the tools plugin directory has no `.wasm` files. 78 + pub async fn plugins_missing(plugin_dir: &Path) -> bool { 79 + let Ok(mut entries) = fs::read_dir(plugin_dir).await else { 80 + return true; 81 + }; 82 + 83 + while let Ok(Some(entry)) = entries.next_entry().await { 84 + if entry.path().extension().is_some_and(|e| e == "wasm") { 85 + return false; 86 + } 87 + } 88 + 89 + true 90 + }
+22 -1
crates/ein-tui/src/app.rs
··· 1 1 // SPDX-License-Identifier: Apache-2.0 2 2 // Copyright 2026 Mason Stallmo 3 3 4 - use ein_proto::ein::{AgentEvent, SessionConfig, SessionSummary, UserInput}; 4 + use ein_proto::ein::{AgentEvent, PluginSourceStatus, SessionConfig, SessionSummary, UserInput}; 5 5 use tokio::sync::{mpsc, oneshot}; 6 6 7 7 use crate::config::ClientConfig; ··· 26 26 SessionsLoaded(Vec<SessionSummary>, oneshot::Sender<SessionConfig>), 27 27 /// A session was successfully deleted; remove it from the session picker. 28 28 SessionDeleted(String), 29 + /// The server returned plugin source statuses for the plugin modal. 30 + PluginStatusLoaded(Vec<PluginSourceStatus>), 31 + /// A plugin install RPC completed; carries success flag and a status message. 32 + PluginInstallResult { success: bool, message: String }, 29 33 } 30 34 31 35 /// Whether the TUI currently has a live server connection. ··· 71 75 // Session picker / CWD prompt state 72 76 // --------------------------------------------------------------------------- 73 77 78 + /// State for the plugin manager modal, opened via `/plugins`. 79 + pub(crate) struct PluginModalState { 80 + /// Plugin sources and their install status, fetched from the server. 81 + pub(crate) sources: Vec<PluginSourceStatus>, 82 + /// Currently highlighted row index. 83 + pub(crate) selected: usize, 84 + /// True while an install RPC is in flight. 85 + pub(crate) installing: bool, 86 + /// True while the initial status check RPC is in flight. 87 + pub(crate) loading: bool, 88 + /// Last install result message, shown beneath the source list. 89 + pub(crate) status_message: Option<String>, 90 + } 91 + 74 92 /// State for the session picker modal shown on first connection. 75 93 pub(crate) struct SessionPickerState { 76 94 /// Existing sessions from the server (newest-first). Index 0 in the UI is ··· 161 179 pub(crate) current_cfg: ClientConfig, 162 180 /// Session UUID assigned by the server, shown in the status bar. 163 181 pub(crate) session_id: Option<String>, 182 + /// When `Some`, the plugin manager modal is visible. 183 + pub(crate) pending_plugin_modal: Option<PluginModalState>, 164 184 } 165 185 166 186 impl App { ··· 190 210 cwd, 191 211 current_cfg, 192 212 session_id: None, 213 + pending_plugin_modal: None, 193 214 } 194 215 } 195 216 }
+33 -3
crates/ein-tui/src/connection.rs
··· 2 2 // Copyright 2026 Mason Stallmo 3 3 4 4 use ein_proto::ein::{ 5 - DeleteSessionRequest, ListSessionsRequest, SessionConfig, UserInput, 6 - agent_client::AgentClient, user_input, 5 + CheckPluginsRequest, DeleteSessionRequest, InstallPluginsRequest, InstallPluginsResponse, 6 + ListSessionsRequest, PluginSourceStatus, SessionConfig, UserInput, agent_client::AgentClient, 7 + user_input, 7 8 }; 8 9 use tokio::sync::{mpsc, oneshot}; 9 10 use tokio_stream::wrappers::ReceiverStream; ··· 206 207 } 207 208 } 208 209 210 + /// Opens a short-lived connection and fetches plugin source statuses. 211 + pub(crate) async fn check_plugins(server_addr: &str) -> anyhow::Result<Vec<PluginSourceStatus>> { 212 + let channel = Channel::from_shared(server_addr.to_string())? 213 + .connect() 214 + .await?; 215 + let mut client = AgentClient::new(channel); 216 + let resp = client 217 + .check_plugins(tonic::Request::new(CheckPluginsRequest {})) 218 + .await?; 219 + Ok(resp.into_inner().sources) 220 + } 221 + 222 + /// Opens a short-lived connection and requests plugin installation for `source_id`. 223 + pub(crate) async fn install_plugins( 224 + server_addr: &str, 225 + source_id: String, 226 + ) -> anyhow::Result<InstallPluginsResponse> { 227 + let channel = Channel::from_shared(server_addr.to_string())? 228 + .connect() 229 + .await?; 230 + let mut client = AgentClient::new(channel); 231 + let resp = client 232 + .install_plugins(tonic::Request::new(InstallPluginsRequest { source_id })) 233 + .await?; 234 + Ok(resp.into_inner()) 235 + } 236 + 209 237 /// Opens a short-lived connection and deletes a session by ID. 210 238 /// 211 239 /// Returns `Ok(())` on success; errors are logged by the caller. 212 240 pub(crate) async fn delete_session(server_addr: &str, session_id: String) -> anyhow::Result<()> { 213 - let channel = Channel::from_shared(server_addr.to_string())?.connect().await?; 241 + let channel = Channel::from_shared(server_addr.to_string())? 242 + .connect() 243 + .await?; 214 244 let mut client = AgentClient::new(channel); 215 245 client 216 246 .delete_session(tonic::Request::new(DeleteSessionRequest { session_id }))
+63
crates/ein-tui/src/input.rs
··· 46 46 name: "/compact", 47 47 description: "Summarize and compact conversation history", 48 48 }, 49 + CommandDef { 50 + name: "/plugins", 51 + description: "Manage installed plugins", 52 + }, 49 53 ]; 50 54 51 55 /// Recomputes `autocomplete_matches` and `autocomplete_active` based on the ··· 84 88 OpenSessionPicker, 85 89 /// The user pressed Shift+D on an existing session in the picker; delete it. 86 90 DeleteSession(String), 91 + /// Open the plugin manager modal and fetch status from the server. 92 + OpenPluginModal, 93 + /// User selected a plugin source to install/update; `source_id` identifies it. 94 + InstallPlugin { source_id: String }, 87 95 } 88 96 89 97 // --------------------------------------------------------------------------- ··· 101 109 return KeyAction::Quit; 102 110 } 103 111 112 + // While the plugin modal is visible, route all key events to it. 113 + if app.pending_plugin_modal.is_some() { 114 + return handle_plugin_modal_key(app, key); 115 + } 116 + 104 117 // While the session picker is visible, route all key events to it. 105 118 if app.pending_session_picker.is_some() { 106 119 return handle_session_picker_key(app, key).await; ··· 112 125 } 113 126 114 127 handle_normal_key(app, key).await 128 + } 129 + 130 + fn handle_plugin_modal_key(app: &mut App, key: KeyEvent) -> KeyAction { 131 + // While an async operation is in flight, only Esc is allowed. 132 + let busy = app 133 + .pending_plugin_modal 134 + .as_ref() 135 + .is_some_and(|m| m.loading || m.installing); 136 + if busy { 137 + if key.code == KeyCode::Esc { 138 + app.pending_plugin_modal = None; 139 + } 140 + return KeyAction::Continue; 141 + } 142 + 143 + match key.code { 144 + KeyCode::Esc => { 145 + app.pending_plugin_modal = None; 146 + } 147 + KeyCode::Up => { 148 + if let Some(modal) = app.pending_plugin_modal.as_mut() { 149 + if modal.selected > 0 { 150 + modal.selected -= 1; 151 + } 152 + } 153 + } 154 + KeyCode::Down => { 155 + if let Some(modal) = app.pending_plugin_modal.as_mut() { 156 + if modal.selected + 1 < modal.sources.len() { 157 + modal.selected += 1; 158 + } 159 + } 160 + } 161 + KeyCode::Enter => { 162 + let source_id = app 163 + .pending_plugin_modal 164 + .as_ref() 165 + .and_then(|m| m.sources.get(m.selected)) 166 + .map(|s| s.id.clone()) 167 + .unwrap_or_else(|| "default".to_string()); 168 + if let Some(modal) = app.pending_plugin_modal.as_mut() { 169 + modal.installing = true; 170 + modal.status_message = None; 171 + } 172 + return KeyAction::InstallPlugin { source_id }; 173 + } 174 + _ => {} 175 + } 176 + KeyAction::Continue 115 177 } 116 178 117 179 async fn handle_session_picker_key(app: &mut App, key: KeyEvent) -> KeyAction { ··· 270 332 } 271 333 "/exit" => return KeyAction::Quit, 272 334 "/new" => return KeyAction::NewSession, 335 + "/plugins" => return KeyAction::OpenPluginModal, 273 336 "/sessions" => return KeyAction::OpenSessionPicker, 274 337 _ => { 275 338 // Reject unrecognized slash commands — display a local error, do not send to server.
+70 -3
crates/ein-tui/src/lib.rs
··· 12 12 mod input; 13 13 mod render; 14 14 15 - use crate::app::{App, AppEvent, ConnectionStatus, CwdState, DisplayMessage, SessionPickerState}; 15 + use crate::app::{ 16 + App, AppEvent, ConnectionStatus, CwdState, DisplayMessage, PluginModalState, SessionPickerState, 17 + }; 16 18 use crate::config::load_or_create_config; 17 19 use crate::connection::{ 18 - connection_manager, delete_session, spawn_config_watcher, to_proto_session_config, 20 + check_plugins, connection_manager, delete_session, install_plugins, spawn_config_watcher, 21 + to_proto_session_config, 19 22 }; 20 23 use crate::input::{KeyAction, handle_key_event, handle_server_event}; 21 24 use crate::render::render; ··· 146 149 147 150 tokio::select! { 148 151 _ = ticker.tick() => { 149 - if app.agent_busy || matches!(app.connection_status, ConnectionStatus::Connecting) { 152 + let plugin_busy = app 153 + .pending_plugin_modal 154 + .as_ref() 155 + .is_some_and(|m| m.loading || m.installing); 156 + if app.agent_busy 157 + || matches!(app.connection_status, ConnectionStatus::Connecting) 158 + || plugin_busy 159 + { 150 160 app.tick = app.tick.wrapping_add(1); 151 161 } 152 162 } ··· 232 242 } 233 243 }); 234 244 } 245 + KeyAction::OpenPluginModal => { 246 + app.pending_plugin_modal = Some(PluginModalState { 247 + sources: vec![], 248 + selected: 0, 249 + installing: false, 250 + loading: true, 251 + status_message: None, 252 + }); 253 + let addr = args.server_addr.clone(); 254 + let tx = event_tx.clone(); 255 + tokio::spawn(async move { 256 + let sources = check_plugins(&addr).await.unwrap_or_default(); 257 + let _ = tx.send(AppEvent::PluginStatusLoaded(sources)).await; 258 + }); 259 + } 260 + KeyAction::InstallPlugin { source_id } => { 261 + let addr = args.server_addr.clone(); 262 + let tx = event_tx.clone(); 263 + tokio::spawn(async move { 264 + match install_plugins(&addr, source_id).await { 265 + Ok(resp) => { 266 + let _ = tx 267 + .send(AppEvent::PluginInstallResult { 268 + success: resp.success, 269 + message: resp.message, 270 + }) 271 + .await; 272 + } 273 + Err(e) => { 274 + let _ = tx 275 + .send(AppEvent::PluginInstallResult { 276 + success: false, 277 + message: e.to_string(), 278 + }) 279 + .await; 280 + } 281 + } 282 + }); 283 + } 235 284 KeyAction::Continue => {} 236 285 } 237 286 } ··· 284 333 let max_idx = picker.sessions.len(); 285 334 if picker.selected > max_idx { 286 335 picker.selected = max_idx; 336 + } 337 + } 338 + } 339 + AppEvent::PluginStatusLoaded(sources) => { 340 + if let Some(modal) = &mut app.pending_plugin_modal { 341 + modal.sources = sources; 342 + modal.loading = false; 343 + } 344 + } 345 + AppEvent::PluginInstallResult { success, message } => { 346 + if let Some(modal) = &mut app.pending_plugin_modal { 347 + modal.installing = false; 348 + modal.status_message = Some(message); 349 + // Optimistically mark the source as installed on success. 350 + if success { 351 + for source in &mut modal.sources { 352 + source.installed = true; 353 + } 287 354 } 288 355 } 289 356 }
+133 -1
crates/ein-tui/src/render.rs
··· 17 17 }; 18 18 use tracing::debug; 19 19 20 - use crate::app::{App, ConnectionStatus, DisplayMessage, SessionPickerState}; 20 + use crate::app::{App, ConnectionStatus, DisplayMessage, PluginModalState, SessionPickerState}; 21 21 use crate::input::COMMANDS; 22 22 23 23 // --------------------------------------------------------------------------- ··· 303 303 frame.render_widget(List::new(items), layout[2]); 304 304 } 305 305 306 + // --- Plugin modal --- 307 + if let Some(modal) = &app.pending_plugin_modal { 308 + render_plugin_modal(modal, app.tick, frame); 309 + } 310 + 306 311 // --- Session picker (overlays everything, shown before CWD modal) --- 307 312 if let Some(picker) = &app.pending_session_picker { 308 313 render_session_picker(picker, frame); ··· 545 550 let y = area.y + area.height.saturating_sub(height) / 2; 546 551 547 552 Rect::new(x, y, width.min(area.width), height.min(area.height)) 553 + } 554 + 555 + /// Renders the plugin manager modal over the entire terminal. 556 + fn render_plugin_modal(modal: &PluginModalState, tick: u64, frame: &mut Frame) { 557 + let frame_idx = (tick as usize) % SPINNER.len(); 558 + 559 + // Calculate height: borders(2) + top blank(1) + content rows + bottom blank(1) + hints(1) 560 + // plus an optional status row and its trailing blank (2 more). 561 + let content_rows: u16 = if modal.loading || modal.installing { 562 + 1 563 + } else { 564 + modal.sources.len().max(1) as u16 565 + }; 566 + let status_rows: u16 = if modal.status_message.is_some() { 2 } else { 0 }; 567 + let modal_height = 2 + 1 + content_rows + 1 + 1 + status_rows; 568 + let modal_width = (frame.area().width * 7 / 10) 569 + .max(50) 570 + .min(frame.area().width); 571 + let area = centered_rect(modal_width, modal_height, frame.area()); 572 + 573 + frame.render_widget(Clear, area); 574 + 575 + let block = Block::default() 576 + .title(" Plugins ") 577 + .borders(Borders::ALL) 578 + .border_style(Style::default().fg(INPUT_BORDER_COLOR)); 579 + let inner = block.inner(area); 580 + frame.render_widget(block, area); 581 + 582 + let mut lines: Vec<Line> = vec![Line::raw("")]; 583 + 584 + if modal.loading { 585 + lines.push(Line::from(vec![ 586 + Span::styled( 587 + format!(" {} ", SPINNER[frame_idx]), 588 + Style::default().fg(THINKING_COLOR), 589 + ), 590 + Span::styled( 591 + "Loading...", 592 + Style::default() 593 + .fg(MUTED_COLOR) 594 + .add_modifier(Modifier::ITALIC), 595 + ), 596 + ])); 597 + } else if modal.installing { 598 + lines.push(Line::from(vec![ 599 + Span::styled( 600 + format!(" {} ", SPINNER[frame_idx]), 601 + Style::default().fg(THINKING_COLOR), 602 + ), 603 + Span::styled( 604 + "Installing...", 605 + Style::default() 606 + .fg(THINKING_COLOR) 607 + .add_modifier(Modifier::ITALIC), 608 + ), 609 + ])); 610 + } else if modal.sources.is_empty() { 611 + lines.push(Line::from(Span::styled( 612 + " No plugin sources available", 613 + Style::default().fg(MUTED_COLOR), 614 + ))); 615 + } else { 616 + for (i, source) in modal.sources.iter().enumerate() { 617 + let is_sel = modal.selected == i; 618 + let cursor = if is_sel { "> " } else { " " }; 619 + let (checkmark, check_color) = if source.installed { 620 + ("✓", Color::Green) 621 + } else { 622 + ("○", MUTED_COLOR) 623 + }; 624 + let name_style = if is_sel { 625 + Style::default().fg(AUTOCOMPLETE_TOP_COLOR) 626 + } else { 627 + Style::default().fg(MUTED_COLOR) 628 + }; 629 + lines.push(Line::from(vec![ 630 + Span::styled(cursor, name_style), 631 + Span::styled(checkmark, Style::default().fg(check_color)), 632 + Span::styled(format!(" {}", source.display_name), name_style), 633 + ])); 634 + } 635 + } 636 + 637 + lines.push(Line::raw("")); 638 + 639 + if let Some(msg) = &modal.status_message { 640 + let msg_color = 641 + if msg.to_lowercase().contains("fail") || msg.to_lowercase().contains("error") { 642 + DISCONNECTED_COLOR 643 + } else { 644 + Color::Green 645 + }; 646 + lines.push(Line::from(Span::styled( 647 + format!(" {msg}"), 648 + Style::default().fg(msg_color), 649 + ))); 650 + lines.push(Line::raw("")); 651 + } 652 + 653 + if !modal.loading { 654 + lines.push(Line::from(vec![ 655 + Span::styled( 656 + "[↑↓]", 657 + Style::default() 658 + .fg(AUTOCOMPLETE_TOP_COLOR) 659 + .add_modifier(Modifier::BOLD), 660 + ), 661 + Span::styled(" Navigate ", Style::default().fg(MUTED_COLOR)), 662 + Span::styled( 663 + "[Enter]", 664 + Style::default() 665 + .fg(AUTOCOMPLETE_TOP_COLOR) 666 + .add_modifier(Modifier::BOLD), 667 + ), 668 + Span::styled(" Install/Update ", Style::default().fg(MUTED_COLOR)), 669 + Span::styled( 670 + "[Esc]", 671 + Style::default() 672 + .fg(AUTOCOMPLETE_TOP_COLOR) 673 + .add_modifier(Modifier::BOLD), 674 + ), 675 + Span::styled(" Close", Style::default().fg(MUTED_COLOR)), 676 + ])); 677 + } 678 + 679 + frame.render_widget(Paragraph::new(lines), inner); 548 680 } 549 681 550 682 /// Renders the startup modal asking the user whether to allow access to the
+20 -2
crates/ein/src/bin/ein_server.rs
··· 1 1 // SPDX-License-Identifier: Apache-2.0 2 2 // Copyright 2026 Mason Stallmo 3 3 4 - use clap::Parser; 4 + use clap::{Parser, Subcommand}; 5 5 6 6 #[derive(Parser)] 7 7 #[command(author, version, about)] 8 8 struct Args { 9 + #[command(subcommand)] 10 + command: Option<Commands>, 11 + 9 12 /// TCP port for the gRPC server to listen on. 10 13 #[arg(long, default_value = "50051")] 11 14 port: u16, 12 15 } 13 16 17 + #[derive(Subcommand)] 18 + enum Commands { 19 + /// Download and install WASM plugins from the accompanying GitHub release. 20 + InstallPlugins { 21 + /// Plugin version to install (default: current binary version). 22 + #[arg(long)] 23 + version: Option<String>, 24 + }, 25 + } 26 + 14 27 #[tokio::main] 15 28 async fn main() -> anyhow::Result<()> { 16 - ein_server::run(Args::parse().port).await 29 + let args = Args::parse(); 30 + 31 + match args.command { 32 + Some(Commands::InstallPlugins { version }) => ein_server::install_plugins(version).await, 33 + None => ein_server::run(args.port).await, 34 + } 17 35 }