diff --git a/Cargo.lock b/Cargo.lock index be51b95..518cd2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -55,9 +46,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -70,9 +61,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -105,9 +96,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arraydeque" @@ -123,7 +114,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -193,21 +184,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base64" version = "0.21.7" @@ -248,7 +224,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -261,7 +237,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.107", "which", ] @@ -279,11 +255,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -342,9 +318,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.35" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "jobserver", @@ -369,9 +345,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -390,7 +366,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -406,9 +382,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.47" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" dependencies = [ "clap_builder", "clap_derive", @@ -416,9 +392,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" dependencies = [ "anstream", "anstyle", @@ -428,21 +404,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clipboard-win" @@ -595,6 +571,27 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "coolor" version = "1.1.0" @@ -695,7 +692,7 @@ dependencies = [ "proc-macro2", "quote", "strict", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -760,7 +757,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crossterm_winapi", "mio", "parking_lot", @@ -776,13 +773,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crossterm_winapi", "derive_more 2.0.1", "document-features", "mio", "parking_lot", - "rustix 1.0.8", + "rustix 1.1.2", "signal-hook", "signal-hook-mio", "winapi", @@ -834,7 +831,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -845,7 +842,16 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.107", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", ] [[package]] @@ -858,7 +864,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -879,7 +885,7 @@ dependencies = [ "convert_case 0.7.1", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -931,7 +937,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -942,7 +948,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -998,12 +1004,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1027,6 +1033,30 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fantoccini" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a6a7a9a454c24453f9807c7f12b37e31ae43f3eb41888ae1f79a9a3e3be3f5" +dependencies = [ + "base64 0.22.1", + "cookie 0.18.1", + "futures-util", + "http 1.3.1", + "http-body-util", + "hyper 1.7.0", + "hyper-tls 0.6.0", + "hyper-util", + "mime", + "openssl", + "serde", + "serde_json", + "time", + "tokio", + "url", + "webdriver", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1040,7 +1070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.0.8", + "rustix 1.1.2", "windows-sys 0.59.0", ] @@ -1055,9 +1085,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.0" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flate2" @@ -1108,7 +1138,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1188,7 +1218,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -1262,10 +1292,12 @@ dependencies = [ "cocoa", "core-foundation 0.9.4", "core-graphics", + "fantoccini", "image", "objc", "serde", "serde_json", + "shellexpand", "tesseract", "thiserror 1.0.69", "tokio", @@ -1358,9 +1390,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -1374,19 +1406,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasip2", ] [[package]] @@ -1399,12 +1431,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "glob" version = "0.3.3" @@ -1462,6 +1488,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "hashlink" version = "0.8.4" @@ -1603,6 +1635,7 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", ] [[package]] @@ -1618,6 +1651,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.7.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.17" @@ -1625,13 +1674,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "bytes", + "futures-channel", "futures-core", + "futures-util", "http 1.3.1", "http-body 1.0.1", "hyper 1.7.0", + "libc", "pin-project-lite", + "socket2 0.6.1", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1646,7 +1700,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.1", + "windows-core 0.62.2", ] [[package]] @@ -1791,12 +1845,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", ] [[package]] @@ -1828,18 +1882,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", + "syn 2.0.107", ] [[package]] @@ -1906,7 +1949,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] @@ -1921,9 +1964,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.78" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -1960,7 +2003,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2005,35 +2048,35 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.175" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-link", ] [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", ] [[package]] name = "link-cplusplus" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c349c75e1ab4a03bd6b33fe6cbd3c479c5dd443e44ad732664d72cb0e755475" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" dependencies = [ "cc", ] @@ -2046,9 +2089,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -2091,11 +2134,10 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -2140,9 +2182,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -2183,7 +2225,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.59.0", ] @@ -2234,7 +2276,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -2252,13 +2294,19 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -2295,9 +2343,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -2310,23 +2358,14 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -2341,11 +2380,11 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -2362,7 +2401,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2373,9 +2412,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -2401,9 +2440,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2411,15 +2450,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -2448,20 +2487,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -2469,22 +2507,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2", @@ -2536,6 +2574,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2552,7 +2596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -2575,9 +2619,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -2634,7 +2678,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cassowary", "compact_str", "crossterm 0.28.1", @@ -2671,11 +2715,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -2697,14 +2741,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -2714,9 +2758,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -2725,9 +2769,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" @@ -2744,7 +2788,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -2778,7 +2822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.9.4", + "bitflags 2.10.0", "serde", "serde_derive", ] @@ -2793,12 +2837,6 @@ dependencies = [ "ordered-multimap", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -2820,7 +2858,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2829,15 +2867,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] @@ -2857,11 +2895,11 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustyline" -version = "17.0.1" +version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6614df0b6d4cfb20d1d5e295332921793ce499af3ebc011bf1e393380e1e492" +checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "clipboard-win", "fd-lock", @@ -2894,11 +2932,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2913,7 +2951,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -2922,9 +2960,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -2932,15 +2970,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2948,34 +2986,35 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -3105,19 +3144,19 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -3156,7 +3195,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3172,9 +3211,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", @@ -3201,7 +3240,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3227,15 +3266,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -3250,7 +3289,7 @@ dependencies = [ "lazy-regex", "minimad", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "unicode-width 0.1.14", ] @@ -3299,11 +3338,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -3314,18 +3353,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3348,6 +3387,37 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3369,33 +3439,30 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3521,7 +3588,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -3571,9 +3638,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -3583,9 +3650,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -3646,7 +3713,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -3695,19 +3762,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.3+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", @@ -3718,23 +3785,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.51" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -3745,9 +3812,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3755,22 +3822,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -3790,9 +3857,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.78" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -3810,9 +3877,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.1", "jni", @@ -3824,6 +3891,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "webdriver" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144ab979b12d36d65065635e646549925de229954de2eb3b47459b432a42db71" +dependencies = [ + "base64 0.21.7", + "bytes", + "cookie 0.16.2", + "http 0.2.12", + "log", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "time", + "unicode-segmentation", + "url", +] + [[package]] name = "weezl" version = "0.1.10" @@ -3864,7 +3951,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -3894,67 +3981,61 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -3999,16 +4080,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -4059,19 +4140,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.1.3", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -4094,9 +4175,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -4118,9 +4199,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -4142,9 +4223,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -4154,9 +4235,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -4178,9 +4259,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -4202,9 +4283,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -4226,9 +4307,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -4250,9 +4331,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -4275,9 +4356,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -4326,28 +4407,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] @@ -4367,7 +4448,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", "synstructure", ] @@ -4401,7 +4482,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.107", ] [[package]] diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index 3b1a50d..29d2f2a 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -321,6 +321,10 @@ pub struct Cli { /// Disable log file creation (no logs/ directory or session logs) #[arg(long)] pub quiet: bool, + + /// Enable WebDriver tools for browser automation (Safari) + #[arg(long)] + pub webdriver: bool, } pub async fn run() -> Result<()> { @@ -409,12 +413,17 @@ pub async fn run() -> Result<()> { } // Load configuration with CLI overrides - let config = Config::load_with_overrides( + let mut config = Config::load_with_overrides( cli.config.as_deref(), cli.provider.clone(), cli.model.clone(), )?; + // Override webdriver setting from CLI flag + if cli.webdriver { + config.webdriver.enabled = true; + } + // Validate provider if specified if let Some(ref provider) = cli.provider { let valid_providers = ["anthropic", "databricks", "embedded", "openai"]; diff --git a/crates/g3-computer-control/Cargo.toml b/crates/g3-computer-control/Cargo.toml index ea4432d..9aa522c 100644 --- a/crates/g3-computer-control/Cargo.toml +++ b/crates/g3-computer-control/Cargo.toml @@ -13,9 +13,13 @@ serde_json = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } +shellexpand = "3.1" # Async trait support async-trait = "0.1" +# WebDriver support +fantoccini = "0.21" + # OCR dependencies tesseract = "0.14" diff --git a/crates/g3-computer-control/examples/safari_demo.rs b/crates/g3-computer-control/examples/safari_demo.rs new file mode 100644 index 0000000..aed4c1e --- /dev/null +++ b/crates/g3-computer-control/examples/safari_demo.rs @@ -0,0 +1,64 @@ +use g3_computer_control::SafariDriver; +use g3_computer_control::webdriver::WebDriverController; +use anyhow::Result; + +#[tokio::main] +async fn main() -> Result<()> { + println!("Safari WebDriver Demo"); + println!("=====================\n"); + + println!("Make sure to:"); + println!("1. Enable 'Allow Remote Automation' in Safari's Develop menu"); + println!("2. Run: /usr/bin/safaridriver --enable"); + println!("3. Start safaridriver in another terminal: safaridriver --port 4444\n"); + + println!("Connecting to SafariDriver..."); + let mut driver = SafariDriver::new().await?; + println!("✅ Connected!\n"); + + // Navigate to a website + println!("Navigating to example.com..."); + driver.navigate("https://example.com").await?; + println!("✅ Navigated\n"); + + // Get page title + let title = driver.title().await?; + println!("Page title: {}\n", title); + + // Get current URL + let url = driver.current_url().await?; + println!("Current URL: {}\n", url); + + // Find an element + println!("Finding h1 element..."); + let mut h1 = driver.find_element("h1").await?; + let h1_text = h1.text().await?; + println!("H1 text: {}\n", h1_text); + + // Find all paragraphs + println!("Finding all paragraphs..."); + let paragraphs = driver.find_elements("p").await?; + println!("Found {} paragraphs\n", paragraphs.len()); + + // Get page source + println!("Getting page source..."); + let source = driver.page_source().await?; + println!("Page source length: {} bytes\n", source.len()); + + // Execute JavaScript + println!("Executing JavaScript..."); + let result = driver.execute_script("return document.title", vec![]).await?; + println!("JS result: {:?}\n", result); + + // Take a screenshot + println!("Taking screenshot..."); + driver.screenshot("/tmp/safari_demo.png").await?; + println!("✅ Screenshot saved to /tmp/safari_demo.png\n"); + + // Close the browser + println!("Closing browser..."); + driver.quit().await?; + println!("✅ Done!"); + + Ok(()) +} diff --git a/crates/g3-computer-control/src/lib.rs b/crates/g3-computer-control/src/lib.rs index 9bfd0a4..5c72d65 100644 --- a/crates/g3-computer-control/src/lib.rs +++ b/crates/g3-computer-control/src/lib.rs @@ -1,5 +1,9 @@ pub mod types; pub mod platform; +pub mod webdriver; + +// Re-export webdriver types for convenience +pub use webdriver::{WebDriverController, WebElement, safari::SafariDriver}; use anyhow::Result; use async_trait::async_trait; @@ -7,32 +11,12 @@ use types::*; #[async_trait] pub trait ComputerController: Send + Sync { - // Mouse operations - async fn move_mouse(&self, x: i32, y: i32) -> Result<()>; - async fn click(&self, button: MouseButton) -> Result<()>; - async fn double_click(&self, button: MouseButton) -> Result<()>; - - // Keyboard operations - async fn type_text(&self, text: &str) -> Result<()>; - async fn press_key(&self, key: &str) -> Result<()>; - - // Window management - async fn list_windows(&self) -> Result>; - async fn focus_window(&self, window_id: &str) -> Result<()>; - async fn get_window_bounds(&self, window_id: &str) -> Result; - - // UI element inspection - async fn find_element(&self, selector: &ElementSelector) -> Result>; - async fn get_element_text(&self, element_id: &str) -> Result; - async fn get_element_bounds(&self, element_id: &str) -> Result; - // Screen capture async fn take_screenshot(&self, path: &str, region: Option, window_id: Option<&str>) -> Result<()>; // OCR operations - async fn extract_text_from_screen(&self, region: Rect) -> Result; - async fn extract_text_from_image(&self, path: &str) -> Result; - async fn find_text_on_screen(&self, text: &str) -> Result>; + async fn extract_text_from_screen(&self, region: Rect) -> Result; + async fn extract_text_from_image(&self, path: &str) -> Result; } // Platform-specific constructor diff --git a/crates/g3-computer-control/src/platform/macos.rs b/crates/g3-computer-control/src/platform/macos.rs index e489b58..129b73c 100644 --- a/crates/g3-computer-control/src/platform/macos.rs +++ b/crates/g3-computer-control/src/platform/macos.rs @@ -1,310 +1,21 @@ -use crate::{ComputerController, types::*}; +use crate::{ComputerController, types::Rect}; use anyhow::Result; use async_trait::async_trait; -use core_graphics::display::CGPoint; -use core_graphics::event::{CGEvent, CGEventType, CGMouseButton, CGEventTapLocation}; -use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use std::path::Path; use tesseract::Tesseract; -use core_graphics::window::{kCGWindowListOptionOnScreenOnly, kCGNullWindowID, CGWindowListCopyWindowInfo}; -use core_foundation::dictionary::CFDictionary; -use core_foundation::string::CFString; -use core_foundation::base::{TCFType, ToVoid}; -// MacOSController doesn't store CGEventSource to avoid Send/Sync issues -// We create it fresh for each operation pub struct MacOSController { - // Empty struct - event source created per operation + // Empty struct for now } impl MacOSController { pub fn new() -> Result { - // Test that we can create an event source - let _event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) - .map_err(|_| anyhow::anyhow!("Failed to create event source. Make sure Accessibility permissions are granted."))?; Ok(Self {}) } - - fn key_to_keycode(&self, key: &str) -> Result { - // Map key names to macOS keycodes - let keycode = match key.to_lowercase().as_str() { - "return" | "enter" => 36, - "tab" => 48, - "space" => 49, - "delete" | "backspace" => 51, - "escape" | "esc" => 53, - "command" | "cmd" => 55, - "shift" => 56, - "capslock" => 57, - "option" | "alt" => 58, - "control" | "ctrl" => 59, - "left" => 123, - "right" => 124, - "down" => 125, - "up" => 126, - _ => anyhow::bail!("Unknown key: {}", key), - }; - Ok(keycode) - } } #[async_trait] impl ComputerController for MacOSController { - async fn move_mouse(&self, x: i32, y: i32) -> Result<()> { - let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) - .map_err(|_| anyhow::anyhow!("Failed to create event source"))?; - let point = CGPoint::new(x as f64, y as f64); - let event = CGEvent::new_mouse_event( - event_source, - CGEventType::MouseMoved, - point, - CGMouseButton::Left, - ).map_err(|_| anyhow::anyhow!("Failed to create mouse move event"))?; - - event.post(CGEventTapLocation::HID); - Ok(()) - } - - async fn click(&self, button: MouseButton) -> Result<()> { - let (cg_button, down_type, up_type) = match button { - MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, CGEventType::LeftMouseUp), - MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown, CGEventType::RightMouseUp), - MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, CGEventType::OtherMouseUp), - }; - - let point = { - // Get current mouse position - let temp_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) - .map_err(|_| anyhow::anyhow!("Failed to create event source"))?; - let event = CGEvent::new(temp_source) - .map_err(|_| anyhow::anyhow!("Failed to get mouse position"))?; - let p = event.location(); - p - }; - - { - let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) - .map_err(|_| anyhow::anyhow!("Failed to create event source"))?; - - // Mouse down - let down_event = CGEvent::new_mouse_event( - event_source, - down_type, - point, - cg_button, - ).map_err(|_| anyhow::anyhow!("Failed to create mouse down event"))?; - down_event.post(CGEventTapLocation::HID); - } // event_source and down_event dropped here - - // Small delay - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; - - { - let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) - .map_err(|_| anyhow::anyhow!("Failed to create event source"))?; - - let up_event = CGEvent::new_mouse_event( - event_source, - up_type, - point, - cg_button, - ).map_err(|_| anyhow::anyhow!("Failed to create mouse up event"))?; - up_event.post(CGEventTapLocation::HID); - } // event_source and up_event dropped here - - Ok(()) - } - - async fn double_click(&self, button: MouseButton) -> Result<()> { - self.click(button).await?; - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - self.click(button).await?; - Ok(()) - } - - async fn type_text(&self, text: &str) -> Result<()> { - for ch in text.chars() { - { - let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) - .map_err(|_| anyhow::anyhow!("Failed to create event source"))?; - - // Create keyboard event for character - let event = CGEvent::new_keyboard_event( - event_source, - 0, // keycode (0 for unicode) - true, - ).map_err(|_| anyhow::anyhow!("Failed to create keyboard event"))?; - - // Set unicode string - let mut utf16_buf = [0u16; 2]; - let utf16_slice = ch.encode_utf16(&mut utf16_buf); - let utf16_chars: Vec = utf16_slice.iter().copied().collect(); - - event.set_string_from_utf16_unchecked(utf16_chars.as_slice()); - event.post(CGEventTapLocation::HID); - } // event_source and event dropped here - - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - } - Ok(()) - } - - async fn press_key(&self, key: &str) -> Result<()> { - let keycode = self.key_to_keycode(key)?; - - { - let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) - .map_err(|_| anyhow::anyhow!("Failed to create event source"))?; - - // Key down - let down_event = CGEvent::new_keyboard_event( - event_source, - keycode, - true, - ).map_err(|_| anyhow::anyhow!("Failed to create key down event"))?; - down_event.post(CGEventTapLocation::HID); - } // event_source and down_event dropped here - - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; - - { - let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) - .map_err(|_| anyhow::anyhow!("Failed to create event source"))?; - - // Key up - let up_event = CGEvent::new_keyboard_event( - event_source, - keycode, - false, - ).map_err(|_| anyhow::anyhow!("Failed to create key up event"))?; - up_event.post(CGEventTapLocation::HID); - } // event_source and up_event dropped here - - Ok(()) - } - - async fn list_windows(&self) -> Result> { - let mut windows = Vec::new(); - - unsafe { - let window_list = CGWindowListCopyWindowInfo( - kCGWindowListOptionOnScreenOnly, - kCGNullWindowID - ); - - let array = core_foundation::array::CFArray::::wrap_under_create_rule(window_list); - let count = array.len(); - - for i in 0..count { - let dict = array.get(i).unwrap(); - - // Get window ID - let window_id_key = CFString::from_static_string("kCGWindowNumber"); - let window_id: i64 = if let Some(value) = dict.find(window_id_key.to_void()) { - let num: core_foundation::number::CFNumber = TCFType::wrap_under_get_rule(*value as *const _); - num.to_i64().unwrap_or(0) - } else { - 0 - }; - - // Get owner name (app name) - let owner_key = CFString::from_static_string("kCGWindowOwnerName"); - let app_name: String = if let Some(value) = dict.find(owner_key.to_void()) { - let s: CFString = TCFType::wrap_under_get_rule(*value as *const _); - s.to_string() - } else { - "Unknown".to_string() - }; - - // Get window name/title - let name_key = CFString::from_static_string("kCGWindowName"); - let title: String = if let Some(value) = dict.find(name_key.to_void()) { - let s: CFString = TCFType::wrap_under_get_rule(*value as *const _); - s.to_string() - } else { - "".to_string() - }; - - // Get window bounds - let bounds_key = CFString::from_static_string("kCGWindowBounds"); - let bounds = if let Some(bounds_value) = dict.find(bounds_key.to_void()) { - let bounds_dict: CFDictionary = TCFType::wrap_under_get_rule(*bounds_value as *const _); - - let x_key = CFString::from_static_string("X"); - let y_key = CFString::from_static_string("Y"); - let width_key = CFString::from_static_string("Width"); - let height_key = CFString::from_static_string("Height"); - - let x = if let Some(x_value) = bounds_dict.find(x_key.to_void()) { - let num: core_foundation::number::CFNumber = TCFType::wrap_under_get_rule(*x_value as *const _); - num.to_i32().unwrap_or(0) - } else { 0 }; - let y = if let Some(y_value) = bounds_dict.find(y_key.to_void()) { - let num: core_foundation::number::CFNumber = TCFType::wrap_under_get_rule(*y_value as *const _); - num.to_i32().unwrap_or(0) - } else { 0 }; - let width = if let Some(width_value) = bounds_dict.find(width_key.to_void()) { - let num: core_foundation::number::CFNumber = TCFType::wrap_under_get_rule(*width_value as *const _); - num.to_i32().unwrap_or(0) - } else { 0 }; - let height = if let Some(height_value) = bounds_dict.find(height_key.to_void()) { - let num: core_foundation::number::CFNumber = TCFType::wrap_under_get_rule(*height_value as *const _); - num.to_i32().unwrap_or(0) - } else { 0 }; - - Rect { x, y, width, height } - } else { - Rect { x: 0, y: 0, width: 0, height: 0 } - }; - - // Skip windows without meaningful content (system UI elements, etc.) - if app_name.is_empty() || (title.is_empty() && bounds.width < 100) { - continue; - } - - windows.push(Window { - id: format!("{}:{}", app_name, window_id), - title, - app_name, - bounds, - is_active: false, // We'd need additional API calls to determine this - }); - } - } - - Ok(windows) - } - - async fn focus_window(&self, _window_id: &str) -> Result<()> { - // Note: Full implementation would use NSWorkspace to activate application - tracing::warn!("focus_window not fully implemented on macOS"); - Ok(()) - } - - async fn get_window_bounds(&self, _window_id: &str) -> Result { - // Note: Full implementation would use Accessibility API - tracing::warn!("get_window_bounds not fully implemented on macOS"); - Ok(Rect { x: 0, y: 0, width: 800, height: 600 }) - } - - async fn find_element(&self, _selector: &ElementSelector) -> Result> { - // Note: Full implementation would use macOS Accessibility API - tracing::warn!("find_element not fully implemented on macOS"); - Ok(None) - } - - async fn get_element_text(&self, _element_id: &str) -> Result { - // Note: Full implementation would use Accessibility API - tracing::warn!("get_element_text not fully implemented on macOS"); - Ok(String::new()) - } - - async fn get_element_bounds(&self, _element_id: &str) -> Result { - // Note: Full implementation would use Accessibility API - tracing::warn!("get_element_bounds not fully implemented on macOS"); - Ok(Rect { x: 0, y: 0, width: 100, height: 30 }) - } - async fn take_screenshot(&self, path: &str, region: Option, window_id: Option<&str>) -> Result<()> { // Determine the temporary directory for screenshots let temp_dir = std::env::var("TMPDIR") @@ -321,92 +32,6 @@ impl ComputerController for MacOSController { format!("{}/{}", temp_dir.trim_end_matches('/'), path) }; - // Get the currently focused application before taking screenshot - let current_app = std::process::Command::new("osascript") - .arg("-e") - .arg("tell application \"System Events\" to get name of first application process whose frontmost is true") - .output() - .ok() - .and_then(|output| { - if output.status.success() { - Some(String::from_utf8_lossy(&output.stdout).trim().to_string()) - } else { - None - } - }); - - // Handle application-based window capture - let app_name_opt = window_id.and_then(|id| { - // Extract app name from window_id format "AppName:WindowNumber" - id.split(':').next().map(String::from) - }); - - // If we're capturing a specific window, foreground it first - if let Some(ref app) = app_name_opt { - tracing::debug!("Foregrounding application: {}", app); - let _ = std::process::Command::new("osascript") - .arg("-e") - .arg(format!("tell application \"{}\" to activate", app)) - .output(); - - // Give the window time to come to the front - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - } - - let screenshot_result = if let Some(ref app) = app_name_opt { - // Use screencapture with AppleScript to get window ID - let script = format!( - r#"tell application "{}" to id of window 1"#, - app - ); - - let output = std::process::Command::new("osascript") - .arg("-e") - .arg(&script) - .output()?; - - if output.status.success() { - let window_id_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); - std::process::Command::new("screencapture") - .arg(format!("-l{}", window_id_str)) - .arg("-o") - .arg(&final_path) - .output() - } else { - // Fallback to regular screenshot if we can't get window ID - std::process::Command::new("screencapture") - .arg("-x") - .arg(&final_path) - .output() - } - } else { - // Regular screenshot (full screen or region) - // Use native macOS screencapture command which handles all the format complexities - - // Check if we have Screen Recording permission by attempting a test capture - // If we only get wallpaper/menubar but no windows, we need permission - let needs_permission_check = std::env::var("G3_SKIP_PERMISSION_CHECK").is_err(); - - if needs_permission_check { - // Try to open Screen Recording settings if this is the first screenshot - static PERMISSION_PROMPTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); - - if !PERMISSION_PROMPTED.swap(true, std::sync::atomic::Ordering::Relaxed) { - tracing::warn!("\n=== Screen Recording Permission Required ===\n\ - macOS requires explicit permission to capture window content.\n\ - If screenshots only show wallpaper/menubar (no windows):\n\n\ - 1. Open System Settings > Privacy & Security > Screen Recording\n\ - 2. Enable permission for your terminal (iTerm/Terminal) or g3\n\ - 3. Restart your terminal if needed\n\n\ - Opening Screen Recording settings now...\n"); - - // Try to open the settings (non-blocking) - let _ = std::process::Command::new("open") - .arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") - .spawn(); - } - } - let path_obj = Path::new(&final_path); if let Some(parent) = path_obj.parent() { std::fs::create_dir_all(parent)?; @@ -423,41 +48,34 @@ impl ComputerController for MacOSController { cmd.arg(format!("{},{},{},{}", region.x, region.y, region.width, region.height)); } + if let Some(app_name) = window_id { + // Capture specific window by app name + // Use AppleScript to get window ID + let script = format!(r#"tell application "{}" to id of window 1"#, app_name); + let output = std::process::Command::new("osascript") + .arg("-e") + .arg(&script) + .output()?; + + if output.status.success() { + let window_id_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + cmd.arg(format!("-l{}", window_id_str)); + } + } + cmd.arg(&final_path); - cmd.output() - }?; + let screenshot_result = cmd.output()?; if !screenshot_result.status.success() { let stderr = String::from_utf8_lossy(&screenshot_result.stderr); return Err(anyhow::anyhow!("screencapture failed: {}", stderr)); } - // Re-foreground the original application if we foregrounded a different window - if let Some(ref target_app) = app_name_opt { - if let Some(ref original_app) = current_app { - // Only restore if we actually changed the foreground app - if target_app != original_app { - tracing::debug!("Restoring focus to original application: {}", original_app); - - // Small delay to ensure screenshot is complete - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - - let _ = std::process::Command::new("osascript") - .arg("-e") - .arg(format!("tell application \"{}\" to activate", original_app)) - .output(); - } - } - } - - tracing::debug!("Screenshot saved using screencapture: {}", final_path); - Ok(()) } - - async fn extract_text_from_screen(&self, region: Rect) -> Result { + async fn extract_text_from_screen(&self, region: Rect) -> Result { // Take screenshot of region first let temp_path = format!("/tmp/g3_ocr_{}.png", uuid::Uuid::new_v4()); self.take_screenshot(&temp_path, Some(region), None).await?; @@ -471,7 +89,7 @@ impl ComputerController for MacOSController { Ok(result) } - async fn extract_text_from_image(&self, _path: &str) -> Result { + async fn extract_text_from_image(&self, path: &str) -> Result { // Check if tesseract is available on the system let tesseract_check = std::process::Command::new("which") .arg("tesseract") @@ -497,66 +115,11 @@ impl ComputerController for MacOSController { Windows: Reinstall tesseract and ensure language files are included", e) })?; - let text = tess.set_image(_path) - .map_err(|e| anyhow::anyhow!("Failed to load image '{}': {}", _path, e))? + let text = tess.set_image(path) + .map_err(|e| anyhow::anyhow!("Failed to load image '{}': {}", path, e))? .get_text() .map_err(|e| anyhow::anyhow!("Failed to extract text from image: {}", e))?; - // Get confidence (simplified - would need more complex API calls for per-word confidence) - let confidence = 0.85; // Placeholder - - Ok(OCRResult { - text, - confidence, - bounds: Rect { x: 0, y: 0, width: 0, height: 0 }, // Would need image dimensions - }) + Ok(text) } - - async fn find_text_on_screen(&self, _text: &str) -> Result> { - // Check if tesseract is available on the system - let tesseract_check = std::process::Command::new("which") - .arg("tesseract") - .output(); - - if tesseract_check.is_err() || !tesseract_check.as_ref().unwrap().status.success() { - anyhow::bail!("Tesseract OCR is not installed on your system.\n\n\ - To install tesseract:\n macOS: brew install tesseract\n \ - Linux: sudo apt-get install tesseract-ocr (Ubuntu/Debian)\n \ - sudo yum install tesseract (RHEL/CentOS)\n \ - Windows: Download from https://github.com/UB-Mannheim/tesseract/wiki\n\n\ - After installation, restart your terminal and try again."); - } - - // Take full screen screenshot - let temp_path = format!("/tmp/g3_ocr_search_{}.png", uuid::Uuid::new_v4()); - self.take_screenshot(&temp_path, None, None).await?; - - // Use Tesseract to find text with bounding boxes - let tess = Tesseract::new(None, Some("eng")) - .map_err(|e| { - anyhow::anyhow!("Failed to initialize Tesseract: {}\n\n\ - This usually means:\n1. Tesseract is not properly installed\n\ - 2. Language data files are missing\n\nTo fix:\n \ - macOS: brew reinstall tesseract\n \ - Linux: sudo apt-get install tesseract-ocr-eng\n \ - Windows: Reinstall tesseract and ensure language files are included", e) - })?; - - let full_text = tess.set_image(temp_path.as_str()) - .map_err(|e| anyhow::anyhow!("Failed to load screenshot: {}", e))? - .get_text() - .map_err(|e| anyhow::anyhow!("Failed to extract text from screen: {}", e))?; - - // Clean up temp file - let _ = std::fs::remove_file(&temp_path); - - // Simple text search - full implementation would use get_component_images - // to get bounding boxes for each word - if full_text.contains(_text) { - tracing::warn!("Text found but precise coordinates not available in simplified implementation"); - Ok(Some(Point { x: 0, y: 0 })) - } else { - Ok(None) - } - } -} +} \ No newline at end of file diff --git a/crates/g3-computer-control/src/types.rs b/crates/g3-computer-control/src/types.rs index bb8d625..e7ea40e 100644 --- a/crates/g3-computer-control/src/types.rs +++ b/crates/g3-computer-control/src/types.rs @@ -1,11 +1,5 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Point { - pub x: i32, - pub y: i32, -} - #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct Rect { pub x: i32, @@ -13,53 +7,3 @@ pub struct Rect { pub width: i32, pub height: i32, } - -impl Rect { - pub fn center(&self) -> Point { - Point { - x: self.x + self.width / 2, - y: self.y + self.height / 2, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Window { - pub id: String, - pub title: String, - pub app_name: String, - pub bounds: Rect, - pub is_active: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UIElement { - pub id: String, - pub text: String, - pub role: String, - pub bounds: Rect, - pub enabled: bool, - pub visible: bool, - pub value: Option, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub enum MouseButton { - Left, - Right, - Middle, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ElementSelector { - pub text: Option, - pub role: Option, - pub window_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OCRResult { - pub text: String, - pub confidence: f32, - pub bounds: Rect, -} diff --git a/crates/g3-computer-control/src/webdriver/mod.rs b/crates/g3-computer-control/src/webdriver/mod.rs new file mode 100644 index 0000000..1951bef --- /dev/null +++ b/crates/g3-computer-control/src/webdriver/mod.rs @@ -0,0 +1,111 @@ +pub mod safari; + +use anyhow::Result; +use async_trait::async_trait; +use serde_json::Value; + +/// WebDriver controller for browser automation +#[async_trait] +pub trait WebDriverController: Send + Sync { + /// Navigate to a URL + async fn navigate(&mut self, url: &str) -> Result<()>; + + /// Get the current URL + async fn current_url(&self) -> Result; + + /// Get the page title + async fn title(&self) -> Result; + + /// Find an element by CSS selector + async fn find_element(&mut self, selector: &str) -> Result; + + /// Find multiple elements by CSS selector + async fn find_elements(&mut self, selector: &str) -> Result>; + + /// Execute JavaScript in the browser + async fn execute_script(&mut self, script: &str, args: Vec) -> Result; + + /// Get the page source (HTML) + async fn page_source(&self) -> Result; + + /// Take a screenshot and save to path + async fn screenshot(&mut self, path: &str) -> Result<()>; + + /// Close the current window/tab + async fn close(&mut self) -> Result<()>; + + /// Quit the browser session + async fn quit(self) -> Result<()>; +} + +/// Represents a web element in the DOM +pub struct WebElement { + pub(crate) inner: fantoccini::elements::Element, +} + +impl WebElement { + /// Click the element + pub async fn click(&mut self) -> Result<()> { + self.inner.click().await?; + Ok(()) + } + + /// Send keys/text to the element + pub async fn send_keys(&mut self, text: &str) -> Result<()> { + self.inner.send_keys(text).await?; + Ok(()) + } + + /// Clear the element's content (for input fields) + pub async fn clear(&mut self) -> Result<()> { + self.inner.clear().await?; + Ok(()) + } + + /// Get the element's text content + pub async fn text(&self) -> Result { + Ok(self.inner.text().await?) + } + + /// Get an attribute value + pub async fn attr(&self, name: &str) -> Result> { + Ok(self.inner.attr(name).await?) + } + + /// Get a property value + pub async fn prop(&self, name: &str) -> Result> { + Ok(self.inner.prop(name).await?) + } + + /// Get the element's HTML + pub async fn html(&self, inner: bool) -> Result { + Ok(self.inner.html(inner).await?) + } + + /// Check if element is displayed + pub async fn is_displayed(&self) -> Result { + Ok(self.inner.is_displayed().await?) + } + + /// Check if element is enabled + pub async fn is_enabled(&self) -> Result { + Ok(self.inner.is_enabled().await?) + } + + /// Check if element is selected (for checkboxes/radio buttons) + pub async fn is_selected(&self) -> Result { + Ok(self.inner.is_selected().await?) + } + + /// Find a child element by CSS selector + pub async fn find_element(&mut self, selector: &str) -> Result { + let elem = self.inner.find(fantoccini::Locator::Css(selector)).await?; + Ok(WebElement { inner: elem }) + } + + /// Find multiple child elements by CSS selector + pub async fn find_elements(&mut self, selector: &str) -> Result> { + let elems = self.inner.find_all(fantoccini::Locator::Css(selector)).await?; + Ok(elems.into_iter().map(|inner| WebElement { inner }).collect()) + } +} diff --git a/crates/g3-computer-control/src/webdriver/safari.rs b/crates/g3-computer-control/src/webdriver/safari.rs new file mode 100644 index 0000000..762bd8d --- /dev/null +++ b/crates/g3-computer-control/src/webdriver/safari.rs @@ -0,0 +1,212 @@ +use super::{WebDriverController, WebElement}; +use anyhow::{Context, Result}; +use async_trait::async_trait; +use fantoccini::{Client, ClientBuilder}; +use serde_json::Value; +use std::time::Duration; + +/// SafariDriver WebDriver controller +pub struct SafariDriver { + client: Client, +} + +impl SafariDriver { + /// Create a new SafariDriver instance + /// + /// This will connect to SafariDriver running on the default port (4444). + /// Make sure to enable "Allow Remote Automation" in Safari's Develop menu first. + /// + /// You can start SafariDriver manually with: + /// ```bash + /// /usr/bin/safaridriver --enable + /// ``` + pub async fn new() -> Result { + Self::with_port(4444).await + } + + /// Create a new SafariDriver instance with a custom port + pub async fn with_port(port: u16) -> Result { + let url = format!("http://localhost:{}", port); + + let mut caps = serde_json::Map::new(); + caps.insert("browserName".to_string(), Value::String("safari".to_string())); + + let client = ClientBuilder::native() + .capabilities(caps) + .connect(&url) + .await + .context("Failed to connect to SafariDriver. Make sure SafariDriver is running and 'Allow Remote Automation' is enabled in Safari's Develop menu.")?; + + Ok(Self { client }) + } + + /// Go back in browser history + pub async fn back(&mut self) -> Result<()> { + self.client.back().await?; + Ok(()) + } + + /// Go forward in browser history + pub async fn forward(&mut self) -> Result<()> { + self.client.forward().await?; + Ok(()) + } + + /// Refresh the current page + pub async fn refresh(&mut self) -> Result<()> { + self.client.refresh().await?; + Ok(()) + } + + /// Get all window handles + pub async fn window_handles(&mut self) -> Result> { + let handles = self.client.windows().await?; + Ok(handles.into_iter() + .map(|h| h.into()) + .collect()) + } + + /// Switch to a window by handle + pub async fn switch_to_window(&mut self, handle: &str) -> Result<()> { + let window_handle: fantoccini::wd::WindowHandle = handle.to_string().try_into()?; + self.client.switch_to_window(window_handle).await?; + Ok(()) + } + + /// Get the current window handle + pub async fn current_window_handle(&mut self) -> Result { + Ok(self.client.window().await?.into()) + } + + /// Close the current window + pub async fn close_window(&mut self) -> Result<()> { + self.client.close_window().await?; + Ok(()) + } + + /// Create a new window/tab + pub async fn new_window(&mut self, is_tab: bool) -> Result { + let window_type = if is_tab { "tab" } else { "window" }; + let response = self.client.new_window(window_type == "tab").await?; + Ok(response.handle.into()) + } + + /// Get cookies + pub async fn get_cookies(&mut self) -> Result>> { + Ok(self.client.get_all_cookies().await?) + } + + /// Add a cookie + pub async fn add_cookie(&mut self, cookie: fantoccini::cookies::Cookie<'static>) -> Result<()> { + self.client.add_cookie(cookie).await?; + Ok(()) + } + + /// Delete all cookies + pub async fn delete_all_cookies(&mut self) -> Result<()> { + self.client.delete_all_cookies().await?; + Ok(()) + } + + /// Wait for an element to appear (with timeout) + pub async fn wait_for_element(&mut self, selector: &str, timeout: Duration) -> Result { + let start = std::time::Instant::now(); + let poll_interval = Duration::from_millis(100); + + loop { + if let Ok(elem) = self.find_element(selector).await { + return Ok(elem); + } + + if start.elapsed() >= timeout { + anyhow::bail!("Timeout waiting for element: {}", selector); + } + + tokio::time::sleep(poll_interval).await; + } + } + + /// Wait for an element to be visible (with timeout) + pub async fn wait_for_visible(&mut self, selector: &str, timeout: Duration) -> Result { + let start = std::time::Instant::now(); + let poll_interval = Duration::from_millis(100); + + loop { + if let Ok(elem) = self.find_element(selector).await { + if elem.is_displayed().await.unwrap_or(false) { + return Ok(elem); + } + } + + if start.elapsed() >= timeout { + anyhow::bail!("Timeout waiting for element to be visible: {}", selector); + } + + tokio::time::sleep(poll_interval).await; + } + } +} + +#[async_trait] +impl WebDriverController for SafariDriver { + async fn navigate(&mut self, url: &str) -> Result<()> { + self.client.goto(url).await?; + Ok(()) + } + + async fn current_url(&self) -> Result { + Ok(self.client.current_url().await?.to_string()) + } + + async fn title(&self) -> Result { + Ok(self.client.title().await?) + } + + async fn find_element(&mut self, selector: &str) -> Result { + let elem = self.client.find(fantoccini::Locator::Css(selector)).await + .context(format!("Failed to find element with selector: {}", selector))?; + Ok(WebElement { inner: elem }) + } + + async fn find_elements(&mut self, selector: &str) -> Result> { + let elems = self.client.find_all(fantoccini::Locator::Css(selector)).await?; + Ok(elems.into_iter().map(|inner| WebElement { inner }).collect()) + } + + async fn execute_script(&mut self, script: &str, args: Vec) -> Result { + Ok(self.client.execute(script, args).await?) + } + + async fn page_source(&self) -> Result { + Ok(self.client.source().await?) + } + + async fn screenshot(&mut self, path: &str) -> Result<()> { + let screenshot_data = self.client.screenshot().await?; + + // Expand tilde in path + let expanded_path = shellexpand::tilde(path); + let path_str = expanded_path.as_ref(); + + // Create parent directories if needed + if let Some(parent) = std::path::Path::new(path_str).parent() { + std::fs::create_dir_all(parent) + .context("Failed to create parent directories for screenshot")?; + } + + std::fs::write(path_str, screenshot_data) + .context("Failed to write screenshot to file")?; + + Ok(()) + } + + async fn close(&mut self) -> Result<()> { + self.client.close_window().await?; + Ok(()) + } + + async fn quit(mut self) -> Result<()> { + self.client.close().await?; + Ok(()) + } +} diff --git a/crates/g3-config/src/lib.rs b/crates/g3-config/src/lib.rs index 10f05c4..99a0b87 100644 --- a/crates/g3-config/src/lib.rs +++ b/crates/g3-config/src/lib.rs @@ -7,6 +7,7 @@ pub struct Config { pub providers: ProvidersConfig, pub agent: AgentConfig, pub computer_control: ComputerControlConfig, + pub webdriver: WebDriverConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -70,6 +71,21 @@ pub struct ComputerControlConfig { pub max_actions_per_second: u32, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebDriverConfig { + pub enabled: bool, + pub safari_port: u16, +} + +impl Default for WebDriverConfig { + fn default() -> Self { + Self { + enabled: false, + safari_port: 4444, + } + } +} + impl Default for ComputerControlConfig { fn default() -> Self { Self { @@ -103,6 +119,7 @@ impl Default for Config { timeout_seconds: 60, }, computer_control: ComputerControlConfig::default(), + webdriver: WebDriverConfig::default(), } } } @@ -214,6 +231,7 @@ impl Config { timeout_seconds: 60, }, computer_control: ComputerControlConfig::default(), + webdriver: WebDriverConfig::default(), } } diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 60e7d52..a9f1a80 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -21,6 +21,7 @@ mod error_handling_test; use anyhow::Result; use g3_config::Config; use g3_execution::CodeExecutor; +use g3_computer_control::WebDriverController; use g3_providers::{CompletionRequest, Message, MessageRole, ProviderRegistry, Tool}; #[allow(unused_imports)] use regex::Regex; @@ -529,6 +530,8 @@ pub struct Agent { quiet: bool, computer_controller: Option>, todo_content: std::sync::Arc>, + webdriver_session: std::sync::Arc>>>>, + safaridriver_process: std::sync::Arc>>, } impl Agent { @@ -714,6 +717,8 @@ impl Agent { is_autonomous, quiet, computer_controller, + webdriver_session: std::sync::Arc::new(tokio::sync::RwLock::new(None)), + safaridriver_process: std::sync::Arc::new(tokio::sync::RwLock::new(None)), }) } @@ -1023,7 +1028,7 @@ Template: // Check if provider supports native tool calling and add tools if so let provider = self.providers.get(None)?; let tools = if provider.has_native_tool_calling() { - Some(Self::create_tool_definitions()) + Some(Self::create_tool_definitions(self.config.webdriver.enabled)) } else { None }; @@ -1200,8 +1205,8 @@ Template: } /// Create tool definitions for native tool calling providers - fn create_tool_definitions() -> Vec { - vec![ + fn create_tool_definitions(enable_webdriver: bool) -> Vec { + let mut tools = vec![ Tool { name: "shell".to_string(), description: "Execute shell commands".to_string(), @@ -1296,65 +1301,6 @@ Template: "required": ["summary"] }), }, - Tool { - name: "mouse_click".to_string(), - description: "Click the mouse at specific coordinates".to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "x": { - "type": "integer", - "description": "X coordinate" - }, - "y": { - "type": "integer", - "description": "Y coordinate" - }, - "button": { - "type": "string", - "enum": ["left", "right", "middle"], - "description": "Mouse button to click", - "default": "left" - } - }, - "required": ["x", "y"] - }), - }, - Tool { - name: "type_text".to_string(), - description: "Type text at the current cursor position".to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to type" - } - }, - "required": ["text"] - }), - }, - Tool { - name: "find_element".to_string(), - description: "Find a UI element by text, role, or other attributes".to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to search for" - }, - "role": { - "type": "string", - "description": "Element role (button, textfield, etc.)" - }, - "window_id": { - "type": "string", - "description": "Optional window ID to search in" - } - } - }), - }, Tool { name: "take_screenshot".to_string(), description: "Capture a screenshot of the screen, region, or window. When capturing a specific application window (e.g., 'Safari', 'Terminal'), use the window_id parameter with just the application name. The tool will automatically use the native screencapture command with the application's window ID for a clean capture.".to_string(), @@ -1405,28 +1351,6 @@ Template: } }), }, - Tool { - name: "find_text_on_screen".to_string(), - description: "Find text visually on screen and return its coordinates".to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to search for on screen" - } - }, - "required": ["text"] - }), - }, - Tool { - name: "list_windows".to_string(), - description: "List all currently open windows with their IDs, titles, and application names. Use this to identify which window to interact with before taking screenshots or performing other window-specific operations.".to_string(), - input_schema: json!({ - "type": "object", - "properties": {} - }), - }, Tool { name: "todo_read".to_string(), description: "Read the entire TODO list content. Use this to view current tasks, notes, and any other information stored in the TODO list.".to_string(), @@ -1450,7 +1374,193 @@ Template: "required": ["content"] }), }, - ] + ]; + + // Add WebDriver tools if enabled + if enable_webdriver { + tools.extend(vec![ + Tool { + name: "webdriver_start".to_string(), + description: "Start a Safari WebDriver session for browser automation. Must be called before any other webdriver tools. Requires Safari's 'Allow Remote Automation' to be enabled in Develop menu.".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + Tool { + name: "webdriver_navigate".to_string(), + description: "Navigate to a URL in the browser".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The URL to navigate to (must include protocol, e.g., https://)" + } + }, + "required": ["url"] + }), + }, + Tool { + name: "webdriver_get_url".to_string(), + description: "Get the current URL of the browser".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + Tool { + name: "webdriver_get_title".to_string(), + description: "Get the title of the current page".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + Tool { + name: "webdriver_find_element".to_string(), + description: "Find an element on the page by CSS selector and return its text content".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "selector": { + "type": "string", + "description": "CSS selector to find the element (e.g., 'h1', '.class-name', '#id')" + } + }, + "required": ["selector"] + }), + }, + Tool { + name: "webdriver_find_elements".to_string(), + description: "Find all elements matching a CSS selector and return their text content".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "selector": { + "type": "string", + "description": "CSS selector to find elements" + } + }, + "required": ["selector"] + }), + }, + Tool { + name: "webdriver_click".to_string(), + description: "Click an element on the page".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "selector": { + "type": "string", + "description": "CSS selector for the element to click" + } + }, + "required": ["selector"] + }), + }, + Tool { + name: "webdriver_send_keys".to_string(), + description: "Type text into an input element".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "selector": { + "type": "string", + "description": "CSS selector for the input element" + }, + "text": { + "type": "string", + "description": "Text to type into the element" + }, + "clear_first": { + "type": "boolean", + "description": "Whether to clear the element before typing (default: true)" + } + }, + "required": ["selector", "text"] + }), + }, + Tool { + name: "webdriver_execute_script".to_string(), + description: "Execute JavaScript code in the browser and return the result".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "script": { + "type": "string", + "description": "JavaScript code to execute (use 'return' to return a value)" + } + }, + "required": ["script"] + }), + }, + Tool { + name: "webdriver_get_page_source".to_string(), + description: "Get the HTML source of the current page".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + Tool { + name: "webdriver_screenshot".to_string(), + description: "Take a screenshot of the browser window".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path where to save the screenshot (e.g., '/tmp/screenshot.png')" + } + }, + "required": ["path"] + }), + }, + Tool { + name: "webdriver_back".to_string(), + description: "Navigate back in browser history".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + Tool { + name: "webdriver_forward".to_string(), + description: "Navigate forward in browser history".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + Tool { + name: "webdriver_refresh".to_string(), + description: "Refresh the current page".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + Tool { + name: "webdriver_quit".to_string(), + description: "Close the browser and end the WebDriver session".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + ]); + } + + tools } /// Helper method to stream with retry logic @@ -2011,7 +2121,7 @@ Template: // Ensure tools are included for native providers in subsequent iterations if provider.has_native_tool_calling() { - request.tools = Some(Self::create_tool_definitions()); + request.tools = Some(Self::create_tool_definitions(self.config.webdriver.enabled)); } // Only add to full_response if we haven't already added it @@ -2454,10 +2564,10 @@ Template: if is_image { if let Some(controller) = &self.computer_controller { match controller.extract_text_from_image(path_str).await { - Ok(result) => { + Ok(text) => { return Ok(format!( - "📄 Image file (OCR extracted, confidence: {:.2}):\n{}", - result.confidence, result.text + "📄 Image file (OCR extracted):\n{}", + text )); } Err(e) => { @@ -2817,98 +2927,6 @@ Template: Ok("✅ Turn completed".to_string()) } } - "mouse_click" => { - if let Some(controller) = &self.computer_controller { - let x = tool_call - .args - .get("x") - .and_then(|v| v.as_i64()) - .unwrap_or(0) as i32; - let y = tool_call - .args - .get("y") - .and_then(|v| v.as_i64()) - .unwrap_or(0) as i32; - let button_str = tool_call - .args - .get("button") - .and_then(|v| v.as_str()) - .unwrap_or("left"); - - let button = match button_str { - "left" => g3_computer_control::types::MouseButton::Left, - "right" => g3_computer_control::types::MouseButton::Right, - "middle" => g3_computer_control::types::MouseButton::Middle, - _ => g3_computer_control::types::MouseButton::Left, - }; - - match controller.move_mouse(x, y).await { - Ok(_) => { - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - match controller.click(button).await { - Ok(_) => Ok(format!( - "✅ Clicked {} button at ({}, {})", - button_str, x, y - )), - Err(e) => Ok(format!("❌ Failed to click: {}", e)), - } - } - Err(e) => Ok(format!("❌ Failed to move mouse: {}", e)), - } - } else { - Ok("❌ Computer control not enabled. Set computer_control.enabled = true in config.".to_string()) - } - } - "type_text" => { - if let Some(controller) = &self.computer_controller { - let text = tool_call - .args - .get("text") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("Missing text argument"))?; - - match controller.type_text(text).await { - Ok(_) => Ok(format!("✅ Typed text: {}", text)), - Err(e) => Ok(format!("❌ Failed to type text: {}", e)), - } - } else { - Ok("❌ Computer control not enabled. Set computer_control.enabled = true in config.".to_string()) - } - } - "find_element" => { - if let Some(controller) = &self.computer_controller { - let selector = g3_computer_control::types::ElementSelector { - text: tool_call - .args - .get("text") - .and_then(|v| v.as_str()) - .map(String::from), - role: tool_call - .args - .get("role") - .and_then(|v| v.as_str()) - .map(String::from), - window_id: tool_call - .args - .get("window_id") - .and_then(|v| v.as_str()) - .map(String::from), - }; - - match controller.find_element(&selector).await { - Ok(Some(element)) => match serde_json::to_string_pretty(&element) { - Ok(json) => Ok(format!("✅ Found element:\n{}", json)), - Err(e) => { - Ok(format!("✅ Found element but failed to serialize: {}", e)) - } - }, - Ok(None) => Ok("❌ Element not found".to_string()), - Err(e) => Ok(format!("❌ Failed to find element: {}", e)), - } - } else { - Ok("❌ Computer control not enabled. Set computer_control.enabled = true in config.".to_string()) - } - } "take_screenshot" => { if let Some(controller) = &self.computer_controller { let path = tool_call @@ -2973,10 +2991,7 @@ Template: if let Some(path) = tool_call.args.get("path").and_then(|v| v.as_str()) { // Extract text from image file match controller.extract_text_from_image(path).await { - Ok(result) => Ok(format!( - "✅ Extracted text (confidence: {:.2}):\n{}", - result.confidence, result.text - )), + Ok(text) => Ok(format!("✅ Extracted text:\n{}", text)), Err(e) => Ok(format!("❌ Failed to extract text: {}", e)), } } else if let Some(region_obj) = @@ -2997,10 +3012,7 @@ Template: }; match controller.extract_text_from_screen(region).await { - Ok(result) => Ok(format!( - "✅ Extracted text (confidence: {:.2}):\n{}", - result.confidence, result.text - )), + Ok(text) => Ok(format!("✅ Extracted text:\n{}", text)), Err(e) => Ok(format!("❌ Failed to extract text: {}", e)), } } else { @@ -3010,57 +3022,6 @@ Template: Ok("❌ Computer control not enabled. Set computer_control.enabled = true in config.".to_string()) } } - "find_text_on_screen" => { - if let Some(controller) = &self.computer_controller { - let text = tool_call - .args - .get("text") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow::anyhow!("Missing text argument"))?; - - match controller.find_text_on_screen(text).await { - Ok(Some(point)) => Ok(format!( - "✅ Found text '{}' at coordinates ({}, {})", - text, point.x, point.y - )), - Ok(None) => Ok(format!("❌ Text '{}' not found on screen", text)), - Err(e) => Ok(format!("❌ Failed to search for text: {}", e)), - } - } else { - Ok("❌ Computer control not enabled. Set computer_control.enabled = true in config.".to_string()) - } - } - "list_windows" => { - if let Some(controller) = &self.computer_controller { - match controller.list_windows().await { - Ok(windows) => { - if windows.is_empty() { - Ok("📋 No windows found".to_string()) - } else { - let mut output = format!("📋 Found {} windows:\n\n", windows.len()); - for window in windows { - output.push_str(&format!( - "• **{}** ({}x{})\n ID: `{}`\n Title: {}\n\n", - window.app_name, - window.bounds.width, - window.bounds.height, - window.id, - if window.title.is_empty() { - "(no title)" - } else { - &window.title - } - )); - } - Ok(output) - } - } - Err(e) => Ok(format!("❌ Failed to list windows: {}", e)), - } - } else { - Ok("❌ Computer control not enabled. Set computer_control.enabled = true in config.".to_string()) - } - } "todo_read" => { debug!("Processing todo_read tool call"); let content = self.todo_content.read().await; @@ -3094,6 +3055,446 @@ Template: Ok("❌ Missing content argument".to_string()) } } + "webdriver_start" => { + debug!("Processing webdriver_start tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + // Check if session already exists + let session_guard = self.webdriver_session.read().await; + if session_guard.is_some() { + drop(session_guard); + return Ok("✅ WebDriver session already active".to_string()); + } + drop(session_guard); + + // Check if Safari Remote Automation is enabled + let check_enabled = tokio::process::Command::new("safaridriver") + .arg("--enable") + .output() + .await; + + match check_enabled { + Ok(output) if !output.status.success() => { + return Ok("❌ Safari Remote Automation is not enabled.\n\nTo enable it (one-time setup):\n 1. Run: safaridriver --enable\n 2. Enter your password when prompted\n 3. Try again\n\nAlternatively, enable it manually:\n Safari → Develop → Allow Remote Automation".to_string()); + } + Err(e) => { + return Ok(format!("❌ Failed to check Safari automation status: {}\n\nMake sure safaridriver is installed (it comes with macOS).", e)); + } + _ => { + debug!("Safari Remote Automation is enabled"); + } + } + + // Start safaridriver process + let port = self.config.webdriver.safari_port; + info!("Starting safaridriver on port {}", port); + + let safaridriver_result = tokio::process::Command::new("safaridriver") + .arg("--port") + .arg(port.to_string()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn(); + + let mut safaridriver_process = match safaridriver_result { + Ok(process) => process, + Err(e) => { + return Ok(format!("❌ Failed to start safaridriver: {}\n\nMake sure safaridriver is installed.", e)); + } + }; + + // Wait for safaridriver to start up + info!("Waiting for safaridriver to start..."); + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + + // Connect to SafariDriver + match g3_computer_control::SafariDriver::with_port(port).await { + Ok(driver) => { + let session = std::sync::Arc::new(tokio::sync::Mutex::new(driver)); + *self.webdriver_session.write().await = Some(session); + + // Store the process handle + *self.safaridriver_process.write().await = Some(safaridriver_process); + + info!("WebDriver session started successfully"); + Ok("✅ WebDriver session started successfully! Safari should open automatically.".to_string()) + } + Err(e) => { + // Kill the safaridriver process if connection failed + let _ = safaridriver_process.kill().await; + + Ok(format!("❌ Failed to connect to SafariDriver: {}\n\nThis might be because:\n - Port {} is already in use\n - Safari failed to start\n - Network connectivity issue\n\nTry a different port or check if another safaridriver is running.", e, port)) + } + } + } + "webdriver_navigate" => { + debug!("Processing webdriver_navigate tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + drop(session_guard); + let url = match tool_call.args.get("url").and_then(|v| v.as_str()) { + Some(u) => u, + None => return Ok("❌ Missing url argument".to_string()), + }; + + let mut driver = session.lock().await; + match driver.navigate(url).await { + Ok(_) => Ok(format!("✅ Navigated to {}", url)), + Err(e) => Ok(format!("❌ Failed to navigate: {}", e)), + } + } + "webdriver_get_url" => { + debug!("Processing webdriver_get_url tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let driver = session.lock().await; + match driver.current_url().await { + Ok(url) => Ok(format!("Current URL: {}", url)), + Err(e) => Ok(format!("❌ Failed to get URL: {}", e)), + } + } + "webdriver_get_title" => { + debug!("Processing webdriver_get_title tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let driver = session.lock().await; + match driver.title().await { + Ok(title) => Ok(format!("Page title: {}", title)), + Err(e) => Ok(format!("❌ Failed to get title: {}", e)), + } + } + "webdriver_find_element" => { + debug!("Processing webdriver_find_element tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let selector = match tool_call.args.get("selector").and_then(|v| v.as_str()) { + Some(s) => s, + None => return Ok("❌ Missing selector argument".to_string()), + }; + + let mut driver = session.lock().await; + match driver.find_element(selector).await { + Ok(elem) => { + match elem.text().await { + Ok(text) => Ok(format!("Element text: {}", text)), + Err(e) => Ok(format!("❌ Failed to get element text: {}", e)), + } + } + Err(e) => Ok(format!("❌ Failed to find element '{}': {}", selector, e)), + } + } + "webdriver_find_elements" => { + debug!("Processing webdriver_find_elements tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let selector = match tool_call.args.get("selector").and_then(|v| v.as_str()) { + Some(s) => s, + None => return Ok("❌ Missing selector argument".to_string()), + }; + + let mut driver = session.lock().await; + match driver.find_elements(selector).await { + Ok(elements) => { + let mut results = Vec::new(); + for (i, elem) in elements.iter().enumerate() { + match elem.text().await { + Ok(text) => results.push(format!("[{}]: {}", i, text)), + Err(_) => results.push(format!("[{}]: ", i)), + } + } + Ok(format!("Found {} elements:\n{}", results.len(), results.join("\n"))) + } + Err(e) => Ok(format!("❌ Failed to find elements '{}': {}", selector, e)), + } + } + "webdriver_click" => { + debug!("Processing webdriver_click tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let selector = match tool_call.args.get("selector").and_then(|v| v.as_str()) { + Some(s) => s, + None => return Ok("❌ Missing selector argument".to_string()), + }; + + let mut driver = session.lock().await; + match driver.find_element(selector).await { + Ok(mut elem) => { + match elem.click().await { + Ok(_) => Ok(format!("✅ Clicked element '{}'", selector)), + Err(e) => Ok(format!("❌ Failed to click element: {}", e)), + } + } + Err(e) => Ok(format!("❌ Failed to find element '{}': {}", selector, e)), + } + } + "webdriver_send_keys" => { + debug!("Processing webdriver_send_keys tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let selector = match tool_call.args.get("selector").and_then(|v| v.as_str()) { + Some(s) => s, + None => return Ok("❌ Missing selector argument".to_string()), + }; + + let text = match tool_call.args.get("text").and_then(|v| v.as_str()) { + Some(t) => t, + None => return Ok("❌ Missing text argument".to_string()), + }; + + let clear_first = tool_call.args.get("clear_first") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + let mut driver = session.lock().await; + match driver.find_element(selector).await { + Ok(mut elem) => { + if clear_first { + if let Err(e) = elem.clear().await { + return Ok(format!("❌ Failed to clear element: {}", e)); + } + } + match elem.send_keys(text).await { + Ok(_) => Ok(format!("✅ Sent keys to element '{}'", selector)), + Err(e) => Ok(format!("❌ Failed to send keys: {}", e)), + } + } + Err(e) => Ok(format!("❌ Failed to find element '{}': {}", selector, e)), + } + } + "webdriver_execute_script" => { + debug!("Processing webdriver_execute_script tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let script = match tool_call.args.get("script").and_then(|v| v.as_str()) { + Some(s) => s, + None => return Ok("❌ Missing script argument".to_string()), + }; + + let mut driver = session.lock().await; + match driver.execute_script(script, vec![]).await { + Ok(result) => Ok(format!("Script result: {:?}", result)), + Err(e) => Ok(format!("❌ Failed to execute script: {}", e)), + } + } + "webdriver_get_page_source" => { + debug!("Processing webdriver_get_page_source tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let driver = session.lock().await; + match driver.page_source().await { + Ok(source) => { + // Truncate if too long + if source.len() > 10000 { + Ok(format!("Page source ({} chars, truncated to 10000):\n{}...", source.len(), &source[..10000])) + } else { + Ok(format!("Page source ({} chars):\n{}", source.len(), source)) + } + } + Err(e) => Ok(format!("❌ Failed to get page source: {}", e)), + } + } + "webdriver_screenshot" => { + debug!("Processing webdriver_screenshot tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let path = match tool_call.args.get("path").and_then(|v| v.as_str()) { + Some(p) => p, + None => return Ok("❌ Missing path argument".to_string()), + }; + + let mut driver = session.lock().await; + match driver.screenshot(path).await { + Ok(_) => Ok(format!("✅ Screenshot saved to {}", path)), + Err(e) => Ok(format!("❌ Failed to take screenshot: {}", e)), + } + } + "webdriver_back" => { + debug!("Processing webdriver_back tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let mut driver = session.lock().await; + match driver.back().await { + Ok(_) => Ok("✅ Navigated back".to_string()), + Err(e) => Ok(format!("❌ Failed to navigate back: {}", e)), + } + } + "webdriver_forward" => { + debug!("Processing webdriver_forward tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let mut driver = session.lock().await; + match driver.forward().await { + Ok(_) => Ok("✅ Navigated forward".to_string()), + Err(e) => Ok(format!("❌ Failed to navigate forward: {}", e)), + } + } + "webdriver_refresh" => { + debug!("Processing webdriver_refresh tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + let session_guard = self.webdriver_session.read().await; + let session = match session_guard.as_ref() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session. Call webdriver_start first.".to_string()), + }; + + let mut driver = session.lock().await; + match driver.refresh().await { + Ok(_) => Ok("✅ Page refreshed".to_string()), + Err(e) => Ok(format!("❌ Failed to refresh page: {}", e)), + } + } + "webdriver_quit" => { + debug!("Processing webdriver_quit tool call"); + + if !self.config.webdriver.enabled { + return Ok("❌ WebDriver is not enabled. Use --webdriver flag to enable.".to_string()); + } + + // Take the session + let session = match self.webdriver_session.write().await.take() { + Some(s) => s.clone(), + None => return Ok("❌ No active WebDriver session.".to_string()), + }; + + // Quit the WebDriver session + match std::sync::Arc::try_unwrap(session) { + Ok(mutex) => { + let driver = mutex.into_inner(); + match driver.quit().await { + Ok(_) => { + info!("WebDriver session closed successfully"); + + // Kill the safaridriver process + if let Some(mut process) = self.safaridriver_process.write().await.take() { + if let Err(e) = process.kill().await { + warn!("Failed to kill safaridriver process: {}", e); + } else { + info!("Safaridriver process terminated"); + } + } + + Ok("✅ WebDriver session closed and safaridriver stopped".to_string()) + } + Err(e) => Ok(format!("❌ Failed to quit WebDriver: {}", e)), + } + } + Err(_) => Ok("❌ Cannot quit: WebDriver session is still in use".to_string()), + } + } _ => { warn!("Unknown tool: {}", tool_call.tool); Ok(format!("❓ Unknown tool: {}", tool_call.tool)) @@ -3545,3 +3946,23 @@ mod integration_tests { assert_eq!(result, expected); } } + +// Implement Drop to clean up safaridriver process +impl Drop for Agent { + fn drop(&mut self) { + // Try to kill safaridriver process if it's still running + // We need to use try_lock since we can't await in Drop + if let Ok(mut process_guard) = self.safaridriver_process.try_write() { + if let Some(process) = process_guard.take() { + // Use blocking kill since we can't await in Drop + // This is a best-effort cleanup + let _ = std::process::Command::new("kill") + .arg("-9") + .arg(process.id().unwrap_or(0).to_string()) + .output(); + + debug!("Attempted to clean up safaridriver process on Agent drop"); + } + } + } +}