diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000000..b83d22266a --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000000..eddeddef94 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,695 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rust" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "env_logger", + "log", + "serde", + "serde_json", + "thiserror", + "tokio", + "uuid", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000000..fdeea517cf --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2.0" +async-trait = "0.1" +tokio = { version = "1.49.0", features = ["full"] } +log = "0.4" +env_logger = "0.11" +anyhow = "1.0.100" +uuid = { version = "1.11.0", features = ["v4"] } diff --git a/rust/src/client.rs b/rust/src/client.rs new file mode 100644 index 0000000000..352d0c9df3 --- /dev/null +++ b/rust/src/client.rs @@ -0,0 +1,346 @@ +use crate::jsonrpc::JsonRpcClient; +use crate::session::Session; +use crate::types::*; +use anyhow::{Context, Result, anyhow}; +use serde_json::json; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; +use std::sync::{Arc, Mutex as StdMutex}; + +// Type alias for the boxed RPC client type to reduce complexity +type BoxedRpcClient = Arc, Box>>; + +/// Main client for interacting with the Copilot CLI +pub struct Client { + options: ClientOptions, + rpc_client: Option, + cli_process: Option, + state: Arc>, + sessions: Arc>>>, +} + +impl Client { + /// Create a new client with the given options + pub fn new(options: Option) -> Self { + let options = options.unwrap_or_default(); + + Self { + options, + rpc_client: None, + cli_process: None, + state: Arc::new(StdMutex::new(ConnectionState::Disconnected)), + sessions: Arc::new(StdMutex::new(HashMap::new())), + } + } + + /// Start the client and connect to the CLI server + pub fn start(&mut self) -> Result<()> { + { + let mut state = self.state.lock().unwrap(); + if *state != ConnectionState::Disconnected { + return Err(anyhow!("Client is already started or connecting")); + } + *state = ConnectionState::Connecting; + } + + // Start CLI server if needed + if let Some(cli_url) = self.options.cli_url.clone() { + // Connect to external server + self.connect_to_external_server(&cli_url)?; + } else if self.options.use_stdio { + // Start CLI server with stdio + self.start_cli_server()?; + } else { + // Start CLI server with TCP (not yet implemented) + return Err(anyhow!("TCP transport not yet implemented")); + } + + // Set state to connected + { + let mut state = self.state.lock().unwrap(); + *state = ConnectionState::Connected; + } + + Ok(()) + } + + /// Stop the client and cleanup + pub fn stop(&mut self) -> Vec { + let mut errors = Vec::new(); + + // Stop RPC client + if let Some(ref rpc_client) = self.rpc_client { + rpc_client.stop(); + } + + // Stop CLI process + if let Some(ref mut process) = self.cli_process { + if let Err(e) = process.kill() { + errors.push(anyhow!("Failed to kill CLI process: {}", e)); + } + if let Err(e) = process.wait() { + errors.push(anyhow!("Failed to wait for CLI process: {}", e)); + } + } + + self.rpc_client = None; + self.cli_process = None; + + let mut state = self.state.lock().unwrap(); + *state = ConnectionState::Disconnected; + + errors + } + + /// Forcefully stop the client + pub fn force_stop(&mut self) { + let _ = self.stop(); + } + + /// Get the current connection state + pub fn get_state(&self) -> ConnectionState { + *self.state.lock().unwrap() + } + + /// Send a ping request + pub async fn ping(&self, message: &str) -> Result { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let result = rpc_client + .request("ping", json!({ "message": message })) + .await + .map_err(|e| anyhow!("Ping failed: {}", e))?; + + serde_json::from_value(result).context("Failed to parse ping response") + } + + /// Get server status + pub async fn get_status(&self) -> Result { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let result = rpc_client + .request("status.get", json!({})) + .await + .map_err(|e| anyhow!("Get status failed: {}", e))?; + + serde_json::from_value(result).context("Failed to parse status response") + } + + /// Get authentication status + pub async fn get_auth_status(&self) -> Result { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let result = rpc_client + .request("auth.getStatus", json!({})) + .await + .map_err(|e| anyhow!("Get auth status failed: {}", e))?; + + serde_json::from_value(result).context("Failed to parse auth status response") + } + + /// List available models + pub async fn list_models(&self) -> Result> { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let result = rpc_client + .request("models.list", json!({})) + .await + .map_err(|e| anyhow!("List models failed: {}", e))?; + + let response: GetModelsResponse = + serde_json::from_value(result).context("Failed to parse models response")?; + + Ok(response.models) + } + + /// Create a new session + pub async fn create_session(&self, config: Option) -> Result> { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let config = config.unwrap_or_default(); + + // Build session create params + let mut params = json!({}); + if let Some(ref session_id) = config.session_id { + params["sessionId"] = json!(session_id); + } + if let Some(ref model) = config.model { + params["model"] = json!(model); + } + if let Some(ref system_message) = config.system_message { + params["systemMessage"] = serde_json::to_value(system_message)?; + } + if config.streaming { + params["streaming"] = json!(true); + } + + let result = rpc_client + .request("session.create", params) + .await + .map_err(|e| anyhow!("Create session failed: {}", e))?; + + let response: SessionCreateResponse = + serde_json::from_value(result).context("Failed to parse session create response")?; + + let session = Arc::new(Session::new( + response.session_id.clone(), + Arc::clone(rpc_client), + response.workspace_path, + )); + + // Store session + let mut sessions = self.sessions.lock().unwrap(); + sessions.insert(response.session_id.clone(), Arc::clone(&session)); + + Ok(session) + } + + /// Resume an existing session + pub async fn resume_session(&self, session_id: &str) -> Result> { + self.resume_session_with_options(session_id, None).await + } + + /// Resume a session with configuration options + pub async fn resume_session_with_options( + &self, + session_id: &str, + config: Option, + ) -> Result> { + let rpc_client = self + .rpc_client + .as_ref() + .ok_or_else(|| anyhow!("Client not started"))?; + + let mut params = json!({ + "sessionId": session_id + }); + + if let Some(config) = config + && config.streaming + { + params["streaming"] = json!(true); + } + + let result = rpc_client + .request("session.resume", params) + .await + .map_err(|e| anyhow!("Resume session failed: {}", e))?; + + let response: SessionCreateResponse = + serde_json::from_value(result).context("Failed to parse session resume response")?; + + let session = Arc::new(Session::new( + response.session_id.clone(), + Arc::clone(rpc_client), + response.workspace_path, + )); + + // Store session + let mut sessions = self.sessions.lock().unwrap(); + sessions.insert(response.session_id.clone(), Arc::clone(&session)); + + Ok(session) + } + + // ======================================================================== + // Private helper methods + // ======================================================================== + + fn start_cli_server(&mut self) -> Result<()> { + let cli_path = self.options.cli_path.as_deref().unwrap_or("copilot"); + + let mut cmd = Command::new(cli_path); + cmd.arg("agent") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + + // Set working directory + if let Some(ref cwd) = self.options.cwd { + cmd.current_dir(cwd); + } + + // Set log level + if let Some(ref log_level) = self.options.log_level { + cmd.arg("--log-level").arg(log_level); + } + + // Set environment variables + if let Some(ref env_vars) = self.options.env { + for (key, value) in env_vars { + cmd.env(key, value); + } + } + + // Spawn the process + let mut child = cmd.spawn().context("Failed to spawn CLI process")?; + + // Get stdin and stdout handles + let stdin = child + .stdin + .take() + .ok_or_else(|| anyhow!("Failed to get stdin"))?; + let stdout = child + .stdout + .take() + .ok_or_else(|| anyhow!("Failed to get stdout"))?; + + // Create RPC client with boxed writers/readers + let boxed_writer: Box = Box::new(stdin); + let boxed_reader: Box = Box::new(BufReader::new(stdout)); + let rpc_client = JsonRpcClient::new(boxed_writer, boxed_reader); + + // Set up notification handler for session events + let sessions_for_handler = Arc::clone(&self.sessions); + rpc_client.set_notification_handler(Arc::new(move |method, params| { + if method == "session.event" { + // Extract sessionId and event from params + if let Some(session_id) = params.get("sessionId").and_then(|v| v.as_str()) + && let Some(event_value) = params.get("event") + && let Ok(event) = serde_json::from_value::(event_value.clone()) + { + // Dispatch to the session + let sessions = sessions_for_handler.lock().unwrap(); + if let Some(session) = sessions.get(session_id) { + session.dispatch_event(event); + } + } + } + })); + + // Start the RPC client + rpc_client.start(); + + self.rpc_client = Some(Arc::new(rpc_client)); + self.cli_process = Some(child); + + Ok(()) + } + + fn connect_to_external_server(&mut self, _url: &str) -> Result<()> { + // TODO: Implement TCP connection to external server + Err(anyhow!("External server connection not yet implemented")) + } +} + +impl Drop for Client { + fn drop(&mut self) { + let _ = self.stop(); + } +} diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs new file mode 100644 index 0000000000..90900967a0 --- /dev/null +++ b/rust/src/jsonrpc.rs @@ -0,0 +1,428 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Write}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; +use tokio::sync::{mpsc, oneshot}; +use uuid::Uuid; + +// ============================================================================ +// JSON-RPC Types +// ============================================================================ + +/// JSON-RPC error +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcError { + pub code: i32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +impl std::fmt::Display for JsonRpcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "JSON-RPC Error {}: {}", self.code, self.message) + } +} + +impl std::error::Error for JsonRpcError {} + +/// JSON-RPC request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcRequest { + pub jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + pub method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +/// JSON-RPC response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcResponse { + pub jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// JSON-RPC notification (request without ID) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcNotification { + pub jsonrpc: String, + pub method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +// ============================================================================ +// Handler Types +// ============================================================================ + +/// Handler for incoming notifications +pub type NotificationHandler = Arc; + +/// Handler for incoming requests from the server +pub type RequestHandler = Arc Result + Send + Sync>; + +// ============================================================================ +// JSON-RPC Client +// ============================================================================ + +type ResponseSender = oneshot::Sender>; + +pub struct JsonRpcClient { + writer: Arc>, + reader: Arc>, + pending_requests: Arc>>, + request_handlers: Arc>>, + notification_handler: Arc>>, + running: Arc, + stop_tx: mpsc::Sender<()>, + stop_rx: Arc>>>, +} + +impl JsonRpcClient { + /// Create a new JSON-RPC client + pub fn new(writer: W, reader: R) -> Self { + let (stop_tx, stop_rx) = mpsc::channel(1); + + Self { + writer: Arc::new(Mutex::new(writer)), + reader: Arc::new(Mutex::new(reader)), + pending_requests: Arc::new(Mutex::new(HashMap::new())), + request_handlers: Arc::new(Mutex::new(HashMap::new())), + notification_handler: Arc::new(Mutex::new(None)), + running: Arc::new(AtomicBool::new(false)), + stop_tx, + stop_rx: Arc::new(Mutex::new(Some(stop_rx))), + } + } + + /// Start the client and begin processing messages + pub fn start(&self) { + if self + .running + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_err() + { + return; // Already running + } + + let reader = Arc::clone(&self.reader); + let writer = Arc::clone(&self.writer); + let pending_requests = Arc::clone(&self.pending_requests); + let request_handlers = Arc::clone(&self.request_handlers); + let notification_handler = Arc::clone(&self.notification_handler); + let running = Arc::clone(&self.running); + let stop_rx = Arc::clone(&self.stop_rx); + + std::thread::spawn(move || { + let mut stop_rx = stop_rx.lock().unwrap().take().unwrap(); + + loop { + // Check if we should stop + if !running.load(Ordering::SeqCst) { + break; + } + + // Try to receive stop signal (non-blocking) + match stop_rx.try_recv() { + Ok(_) | Err(mpsc::error::TryRecvError::Disconnected) => break, + Err(mpsc::error::TryRecvError::Empty) => {} + } + + // Read next message + let message_result = { + let mut reader_guard = reader.lock().unwrap(); + Self::read_message(&mut *reader_guard) + }; + + match message_result { + Ok(Some(msg)) => { + Self::handle_message( + msg, + &writer, + &pending_requests, + &request_handlers, + ¬ification_handler, + ); + } + Ok(None) => continue, + Err(e) => { + if running.load(Ordering::SeqCst) { + eprintln!("Error reading message: {}", e); + } + break; + } + } + } + + running.store(false, Ordering::SeqCst); + }); + } + + /// Stop the client + pub fn stop(&self) { + self.running.store(false, Ordering::SeqCst); + let _ = self.stop_tx.try_send(()); + } + + /// Set handler for incoming notifications + pub fn set_notification_handler(&self, handler: NotificationHandler) { + let mut guard = self.notification_handler.lock().unwrap(); + *guard = Some(handler); + } + + /// Set handler for incoming server requests + pub fn set_request_handler(&self, method: String, handler: RequestHandler) { + let mut guard = self.request_handlers.lock().unwrap(); + guard.insert(method, handler); + } + + /// Remove handler for a specific method + pub fn remove_request_handler(&self, method: &str) { + let mut guard = self.request_handlers.lock().unwrap(); + guard.remove(method); + } + + /// Send a request and wait for response + pub async fn request(&self, method: &str, params: Value) -> Result { + let request_id = Uuid::new_v4().to_string(); + let (tx, rx) = oneshot::channel(); + + // Register pending request + { + let mut pending = self.pending_requests.lock().unwrap(); + pending.insert(request_id.clone(), tx); + } + + // Send request + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: Some(Value::String(request_id.clone())), + method: method.to_string(), + params: Some(params), + }; + + if let Err(e) = self.send_message(&request) { + // Clean up on error + let mut pending = self.pending_requests.lock().unwrap(); + pending.remove(&request_id); + return Err(JsonRpcError { + code: -32000, + message: format!("Failed to send request: {}", e), + data: None, + }); + } + + // Wait for response + match rx.await { + Ok(result) => result, + Err(_) => Err(JsonRpcError { + code: -32000, + message: "Response channel closed".to_string(), + data: None, + }), + } + } + + /// Send a notification (no response expected) + pub fn notify(&self, method: &str, params: Value) -> Result<()> { + let notification = JsonRpcNotification { + jsonrpc: "2.0".to_string(), + method: method.to_string(), + params: Some(params), + }; + + self.send_message(¬ification) + } + + /// Send a message (internal helper) + fn send_message(&self, message: &T) -> Result<()> { + let data = serde_json::to_vec(message)?; + let header = format!("Content-Length: {}\r\n\r\n", data.len()); + + let mut writer = self.writer.lock().unwrap(); + writer + .write_all(header.as_bytes()) + .context("Failed to write header")?; + writer.write_all(&data).context("Failed to write message")?; + writer.flush().context("Failed to flush writer")?; + + Ok(()) + } + + /// Read a single message from the reader + fn read_message(reader: &mut R) -> Result> { + // Read headers + let mut content_length = 0; + loop { + let mut line = String::new(); + let bytes_read = reader.read_line(&mut line)?; + if bytes_read == 0 { + return Ok(None); // EOF + } + + let line = line.trim(); + if line.is_empty() { + break; // End of headers + } + + if let Some(value) = line.strip_prefix("Content-Length:") { + content_length = value.trim().parse()?; + } + } + + if content_length == 0 { + return Ok(None); + } + + // Read body + let mut body = vec![0u8; content_length]; + reader.read_exact(&mut body)?; + + let value: Value = serde_json::from_slice(&body)?; + Ok(Some(value)) + } + + /// Handle an incoming message + fn handle_message( + msg: Value, + writer: &Arc>, + pending_requests: &Arc>>, + request_handlers: &Arc>>, + notification_handler: &Arc>>, + ) { + // Try to parse as response first + if let Ok(response) = serde_json::from_value::(msg.clone()) { + if let Some(id) = &response.id { + let id_str = match id { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + _ => return, + }; + + let mut pending = pending_requests.lock().unwrap(); + if let Some(tx) = pending.remove(&id_str) { + let result = if let Some(error) = response.error { + Err(error) + } else { + Ok(response.result.unwrap_or(Value::Null)) + }; + let _ = tx.send(result); + } + } + return; + } + + // Try to parse as request + if let Ok(request) = serde_json::from_value::(msg.clone()) + && request.id.is_some() + { + Self::handle_request(request, writer, request_handlers); + return; + } + + // Try to parse as notification + if let Ok(notification) = serde_json::from_value::(msg) { + Self::handle_notification(notification, notification_handler); + } + } + + /// Handle an incoming request from the server + fn handle_request( + request: JsonRpcRequest, + writer: &Arc>, + request_handlers: &Arc>>, + ) { + let handler = { + let handlers = request_handlers.lock().unwrap(); + handlers.get(&request.method).cloned() + }; + + let response = if let Some(handler) = handler { + let result = handler(request.params.unwrap_or(Value::Null)); + match result { + Ok(value) => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id.clone(), + result: Some(value), + error: None, + }, + Err(error) => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id.clone(), + result: None, + error: Some(error), + }, + } + } else { + JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id.clone(), + result: None, + error: Some(JsonRpcError { + code: -32601, + message: format!("Method not found: {}", request.method), + data: None, + }), + } + }; + + // Send response + let data = match serde_json::to_vec(&response) { + Ok(d) => d, + Err(e) => { + eprintln!("Failed to serialize response: {}", e); + return; + } + }; + + let header = format!("Content-Length: {}\r\n\r\n", data.len()); + let mut writer = writer.lock().unwrap(); + if let Err(e) = writer.write_all(header.as_bytes()) { + eprintln!("Failed to write response header: {}", e); + return; + } + if let Err(e) = writer.write_all(&data) { + eprintln!("Failed to write response body: {}", e); + return; + } + let _ = writer.flush(); + } + + /// Handle an incoming notification + fn handle_notification( + notification: JsonRpcNotification, + notification_handler: &Arc>>, + ) { + let handler = notification_handler.lock().unwrap().clone(); + if let Some(handler) = handler { + handler( + notification.method, + notification.params.unwrap_or(Value::Null), + ); + } + } +} + +// ============================================================================ +// Convenience type for stdio transport +// ============================================================================ + +pub type StdioJsonRpcClient = JsonRpcClient>; + +impl StdioJsonRpcClient { + pub fn new_stdio() -> Self { + Self::new(std::io::stdout(), BufReader::new(std::io::stdin())) + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000000..bf30e4e8ae --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod jsonrpc; +pub mod session; +pub mod types; diff --git a/rust/src/session.rs b/rust/src/session.rs new file mode 100644 index 0000000000..a248b89517 --- /dev/null +++ b/rust/src/session.rs @@ -0,0 +1,142 @@ +use crate::jsonrpc::JsonRpcClient; +use crate::types::{MessageOptions, SessionEvent, SessionEventHandler, SessionGetMessagesResponse}; +use anyhow::{Context, Result, anyhow}; +use serde_json::json; +use std::io::{BufRead, Write}; +use std::sync::{Arc, Mutex}; + +// Type alias for the boxed RPC client type (matches client.rs) +type BoxedRpcClient = Arc, Box>>; + +/// Represents a conversation session with the Copilot CLI +pub struct Session { + pub id: String, + workspace_path: Option, + rpc_client: BoxedRpcClient, + event_handlers: Arc>>, +} + +impl Session { + /// Create a new session + pub fn new(id: String, rpc_client: BoxedRpcClient, workspace_path: Option) -> Self { + Self { + id, + workspace_path, + rpc_client, + event_handlers: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Get the workspace path for infinite sessions + pub fn workspace_path(&self) -> Option<&str> { + self.workspace_path.as_deref() + } + + /// Send a message to this session + pub async fn send(&self, options: MessageOptions) -> Result { + let mut params = json!({ + "sessionId": self.id, + "message": { + "prompt": options.prompt + } + }); + + if !options.attachments.is_empty() { + params["message"]["attachments"] = serde_json::to_value(&options.attachments)?; + } + + if let Some(ref mode) = options.mode { + params["message"]["mode"] = json!(mode); + } + + let result = self + .rpc_client + .request("session.send", params) + .await + .map_err(|e| anyhow!("Failed to send message: {}", e))?; + + let response: serde_json::Value = result; + let message_id = response + .get("messageId") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("Response missing messageId"))?; + + Ok(message_id.to_string()) + } + + /// Subscribe to session events + pub fn on(&self, handler: SessionEventHandler) -> impl FnOnce() { + let mut handlers = self.event_handlers.lock().unwrap(); + let index = handlers.len(); + handlers.push(handler); + + // Return unsubscribe function + let event_handlers = Arc::clone(&self.event_handlers); + move || { + let mut handlers = event_handlers.lock().unwrap(); + if index < handlers.len() { + let _ = handlers.remove(index); + } + } + } + + /// Get messages from the session + pub async fn get_messages(&self) -> Result> { + let params = json!({ + "sessionId": self.id + }); + + let result = self + .rpc_client + .request("session.getMessages", params) + .await + .map_err(|e| anyhow!("Failed to get messages: {}", e))?; + + let response: SessionGetMessagesResponse = + serde_json::from_value(result).context("Failed to parse get messages response")?; + + Ok(response.events) + } + + /// Destroy the session + pub async fn destroy(&self) -> Result<()> { + let params = json!({ + "sessionId": self.id + }); + + self.rpc_client + .request("session.destroy", params) + .await + .map_err(|e| anyhow!("Failed to destroy session: {}", e))?; + + Ok(()) + } + + /// Abort the current operation + pub async fn abort(&self) -> Result<()> { + let params = json!({ + "sessionId": self.id + }); + + self.rpc_client + .request("session.abort", params) + .await + .map_err(|e| anyhow!("Failed to abort session: {}", e))?; + + Ok(()) + } + + /// Dispatch an event to all registered handlers + pub fn dispatch_event(&self, event: SessionEvent) { + let handlers = self.event_handlers.lock().unwrap(); + for handler in handlers.iter() { + // Call handler - catch panics to prevent crashing the dispatcher + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + handler(event.clone()); + })); + if let Err(e) = result { + eprintln!("Error in session event handler: {:?}", e); + } + } + } +} diff --git a/rust/src/types.rs b/rust/src/types.rs new file mode 100644 index 0000000000..bf4b703797 --- /dev/null +++ b/rust/src/types.rs @@ -0,0 +1,620 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +// ============================================================================ +// Connection State +// ============================================================================ + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ConnectionState { + Disconnected, + Connecting, + Connected, + Error, +} + +// ============================================================================ +// Client Configuration +// ============================================================================ + +/// ClientOptions configures the Copilot client +#[derive(Debug, Clone, Default)] +pub struct ClientOptions { + /// Path to the Copilot CLI executable (default: "copilot") + pub cli_path: Option, + /// Working directory for the CLI process (default: inherit from current process) + pub cwd: Option, + /// Port for TCP transport (default: 0 = random port) + pub port: Option, + /// Enable stdio transport instead of TCP (default: true) + pub use_stdio: bool, + /// URL of an existing Copilot CLI server to connect to over TCP + /// Format: "host:port", "http://host:port", or just "port" (defaults to localhost) + /// Examples: "localhost:8080", "http://127.0.0.1:9000", "8080" + /// Mutually exclusive with cli_path, use_stdio + pub cli_url: Option, + /// Log level for the CLI server + pub log_level: Option, + /// Automatically starts the CLI server on first use (default: true) + pub auto_start: Option, + /// Automatically restarts the CLI server if it crashes (default: true) + pub auto_restart: Option, + /// Environment variables for the CLI process (default: inherits from current process) + pub env: Option>, +} + +impl ClientOptions { + pub fn new() -> Self { + Self { + cli_path: Some("copilot".to_string()), + use_stdio: true, + log_level: Some("info".to_string()), + auto_start: Some(true), + auto_restart: Some(true), + ..Default::default() + } + } +} + +// ============================================================================ +// System Message Configuration +// ============================================================================ + +/// System message configuration for session creation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SystemMessageConfig { + /// Mode: "append" or "replace" + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + /// Content: additional instructions (append) or complete system message (replace) + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, +} + +// ============================================================================ +// Permission Types +// ============================================================================ + +/// Permission request from the server +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionRequest { + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + #[serde(flatten)] + pub extra: HashMap, +} + +/// Result of a permission request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PermissionRequestResult { + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub rules: Option>, +} + +/// Context for permission invocation +#[derive(Debug, Clone)] +pub struct PermissionInvocation { + pub session_id: String, +} + +/// Handler for permission requests +pub type PermissionHandler = Box< + dyn Fn(PermissionRequest, PermissionInvocation) -> Result + + Send + + Sync, +>; + +// ============================================================================ +// MCP Server Configuration +// ============================================================================ + +/// Configuration for a local/stdio MCP server +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPLocalServerConfig { + pub tools: Vec, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub server_type: Option, // "local" or "stdio" + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + pub command: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub args: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub env: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub cwd: Option, +} + +/// Configuration for a remote MCP server (HTTP or SSE) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPRemoteServerConfig { + pub tools: Vec, + #[serde(rename = "type")] + pub server_type: String, // "http" or "sse" + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub headers: Option>, +} + +/// MCP server configuration (can be local or remote) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MCPServerConfig { + Local(MCPLocalServerConfig), + Remote(MCPRemoteServerConfig), + Raw(HashMap), +} + +// ============================================================================ +// Custom Agent Configuration +// ============================================================================ + +/// Configuration for a custom agent +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomAgentConfig { + /// Unique name of the custom agent + pub name: String, + /// Display name for UI purposes + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Description of what the agent does + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// List of tool names the agent can use (None for all tools) + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, + /// Prompt content for the agent + pub prompt: String, + /// MCP servers specific to this agent + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp_servers: Option>, + /// Whether the agent should be available for model inference + #[serde(skip_serializing_if = "Option::is_none")] + pub infer: Option, +} + +// ============================================================================ +// Infinite Session Configuration +// ============================================================================ + +/// Configuration for infinite sessions with automatic context compaction +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InfiniteSessionConfig { + /// Controls whether infinite sessions are enabled (default: true) + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + /// Context utilization (0.0-1.0) at which background compaction starts (default: 0.80) + #[serde(skip_serializing_if = "Option::is_none")] + pub background_compaction_threshold: Option, + /// Context utilization (0.0-1.0) at which the session blocks until compaction completes (default: 0.95) + #[serde(skip_serializing_if = "Option::is_none")] + pub buffer_exhaustion_threshold: Option, +} + +// ============================================================================ +// Provider Configuration +// ============================================================================ + +/// Azure-specific provider options +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AzureProviderOptions { + /// Azure API version (default: "2024-10-21") + #[serde(skip_serializing_if = "Option::is_none")] + pub api_version: Option, +} + +/// Configuration for a custom model provider +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderConfig { + /// Provider type: "openai", "azure", or "anthropic" (default: "openai") + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub provider_type: Option, + /// API format (openai/azure only): "completions" or "responses" (default: "completions") + #[serde(skip_serializing_if = "Option::is_none")] + pub wire_api: Option, + /// API endpoint URL + pub base_url: String, + /// API key (optional for local providers like Ollama) + #[serde(skip_serializing_if = "Option::is_none")] + pub api_key: Option, + /// Bearer token for authentication (takes precedence over api_key) + #[serde(skip_serializing_if = "Option::is_none")] + pub bearer_token: Option, + /// Azure-specific options + #[serde(skip_serializing_if = "Option::is_none")] + pub azure: Option, +} + +// ============================================================================ +// Tool Types +// ============================================================================ + +/// A tool that can be invoked by Copilot +#[derive(Clone)] +pub struct Tool { + pub name: String, + pub description: Option, + pub parameters: HashMap, + pub handler: ToolHandler, +} + +impl std::fmt::Debug for Tool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tool") + .field("name", &self.name) + .field("description", &self.description) + .field("parameters", &self.parameters) + .field("handler", &"") + .finish() + } +} + +/// Context for a tool invocation +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolInvocation { + pub session_id: String, + pub tool_call_id: String, + pub tool_name: String, + pub arguments: Value, +} + +/// Handler for tool invocations +pub type ToolHandler = + std::sync::Arc Result + Send + Sync>; + +/// Binary result for tools +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolBinaryResult { + pub data: String, + pub mime_type: String, + #[serde(rename = "type")] + pub result_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// Result of a tool invocation +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolResult { + pub text_result_for_llm: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub binary_results_for_llm: Option>, + pub result_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub session_log: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_telemetry: Option>, +} + +// ============================================================================ +// Session Configuration +// ============================================================================ + +/// Configuration for creating a new session +#[derive(Default)] +pub struct SessionConfig { + /// Optional custom session ID + pub session_id: Option, + /// Model to use for this session + pub model: Option, + /// Override the default configuration directory location + pub config_dir: Option, + /// Caller-implemented tools to expose to the CLI + pub tools: Vec, + /// System message customization + pub system_message: Option, + /// List of tool names to allow (takes precedence over excluded_tools) + pub available_tools: Option>, + /// List of tool names to disable + pub excluded_tools: Option>, + /// Handler for permission requests + pub on_permission_request: Option, + /// Enable streaming of assistant message and reasoning chunks + pub streaming: bool, + /// Custom model provider configuration (BYOK) + pub provider: Option, + /// MCP servers for the session + pub mcp_servers: Option>, + /// Custom agents for the session + pub custom_agents: Option>, + /// Directories to load skills from + pub skill_directories: Option>, + /// Skill names to disable + pub disabled_skills: Option>, + /// Infinite sessions configuration + pub infinite_sessions: Option, +} + +/// Configuration for resuming a session +#[derive(Default)] +pub struct ResumeSessionConfig { + /// Caller-implemented tools to expose to the CLI + pub tools: Vec, + /// Custom model provider configuration + pub provider: Option, + /// Handler for permission requests + pub on_permission_request: Option, + /// Enable streaming + pub streaming: bool, + /// MCP servers for the session + pub mcp_servers: Option>, + /// Custom agents for the session + pub custom_agents: Option>, + /// Directories to load skills from + pub skill_directories: Option>, + /// Skill names to disable + pub disabled_skills: Option>, +} + +// ============================================================================ +// Message Types +// ============================================================================ + +/// Attachment type +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AttachmentType { + File, + Directory, +} + +/// Attachment for a message +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Attachment { + pub display_name: String, + pub path: String, + #[serde(rename = "type")] + pub attachment_type: AttachmentType, +} + +/// Options for sending a message +#[derive(Debug, Clone)] +pub struct MessageOptions { + /// The message prompt + pub prompt: String, + /// File or directory attachments + pub attachments: Vec, + /// Message delivery mode (default: "enqueue") + pub mode: Option, +} + +// ============================================================================ +// Response Types +// ============================================================================ + +/// Response from a ping request +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PingResponse { + pub message: String, + pub timestamp: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol_version: Option, +} + +/// Response from session.create +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCreateResponse { + pub session_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_path: Option, +} + +/// Response from session.send +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionSendResponse { + pub message_id: String, +} + +/// Response from status.get +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetStatusResponse { + pub version: String, + pub protocol_version: i32, +} + +/// Response from auth.getStatus +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAuthStatusResponse { + pub is_authenticated: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub host: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub login: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_message: Option, +} + +/// Vision-specific limits +#[derive(Debug, Clone, Deserialize)] +pub struct ModelVisionLimits { + pub supported_media_types: Vec, + pub max_prompt_images: i32, + pub max_prompt_image_size: i32, +} + +/// Model limits +#[derive(Debug, Clone, Deserialize)] +pub struct ModelLimits { + #[serde(skip_serializing_if = "Option::is_none")] + pub max_prompt_tokens: Option, + pub max_context_window_tokens: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub vision: Option, +} + +/// Model support flags +#[derive(Debug, Clone, Deserialize)] +pub struct ModelSupports { + pub vision: bool, +} + +/// Model capabilities +#[derive(Debug, Clone, Deserialize)] +pub struct ModelCapabilities { + pub supports: ModelSupports, + pub limits: ModelLimits, +} + +/// Model policy +#[derive(Debug, Clone, Deserialize)] +pub struct ModelPolicy { + pub state: String, + pub terms: String, +} + +/// Model billing information +#[derive(Debug, Clone, Deserialize)] +pub struct ModelBilling { + pub multiplier: f64, +} + +/// Information about a model +#[derive(Debug, Clone, Deserialize)] +pub struct ModelInfo { + pub id: String, + pub name: String, + pub capabilities: ModelCapabilities, + #[serde(skip_serializing_if = "Option::is_none")] + pub policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub billing: Option, +} + +/// Response from models.list +#[derive(Debug, Clone, Deserialize)] +pub struct GetModelsResponse { + pub models: Vec, +} + +/// Response from session.getMessages +#[derive(Debug, Clone, Deserialize)] +pub struct SessionGetMessagesResponse { + pub events: Vec, +} + +// ============================================================================ +// Session Events (simplified for now) +// ============================================================================ + +/// Session event (simplified - will be expanded with generated types later) +#[derive(Debug, Clone, Deserialize)] +pub struct SessionEvent { + #[serde(rename = "type")] + pub event_type: String, + pub id: String, + #[serde(flatten)] + pub data: Value, +} + +/// Handler for session events +pub type SessionEventHandler = Box; + +// ============================================================================ +// Logging and Events +// ============================================================================ + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LogMessage { + pub level: LogLevel, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Debug, + Info, + Warn, + Error, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentConfig { + pub agent_id: String, + pub agent_name: String, + pub version: String, + #[serde(default)] + pub capabilities: Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProtocolEvent { + pub event: String, + #[serde(flatten)] + pub payload: Value, +} + +// ============================================================================ +// Manual Debug Implementations for types with function pointers +// ============================================================================ + +impl std::fmt::Debug for SessionConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SessionConfig") + .field("session_id", &self.session_id) + .field("model", &self.model) + .field("config_dir", &self.config_dir) + .field("tools", &format!("[{} tools]", self.tools.len())) + .field("system_message", &self.system_message) + .field("available_tools", &self.available_tools) + .field("excluded_tools", &self.excluded_tools) + .field( + "on_permission_request", + &self.on_permission_request.as_ref().map(|_| ""), + ) + .field("streaming", &self.streaming) + .field("provider", &self.provider) + .field("mcp_servers", &self.mcp_servers) + .field("custom_agents", &self.custom_agents) + .field("skill_directories", &self.skill_directories) + .field("disabled_skills", &self.disabled_skills) + .field("infinite_sessions", &self.infinite_sessions) + .finish() + } +} + +impl std::fmt::Debug for ResumeSessionConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResumeSessionConfig") + .field("tools", &format!("[{} tools]", self.tools.len())) + .field("provider", &self.provider) + .field( + "on_permission_request", + &self.on_permission_request.as_ref().map(|_| ""), + ) + .field("streaming", &self.streaming) + .field("mcp_servers", &self.mcp_servers) + .field("custom_agents", &self.custom_agents) + .field("skill_directories", &self.skill_directories) + .field("disabled_skills", &self.disabled_skills) + .finish() + } +}