From c2c8c908954397f9f2019d0431c6da3b4f3b9e80 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 5 Sep 2025 16:02:11 -0700 Subject: [PATCH 1/3] add spider --- .gitmodules | 3 + Cargo.lock | 351 +- Cargo.toml | 3 +- README.md | 2 +- hyperdrive/Cargo.toml | 2 +- .../dependencies/anthropic-api-key-manager | 1 + hyperdrive/packages-build-parameters.json | 14 +- hyperdrive/packages/file-explorer/Cargo.lock | 257 +- .../file-explorer/explorer/Cargo.toml | 9 +- .../file-explorer/explorer/src/lib.rs | 18 +- .../components/ContextMenu/ContextMenu.tsx | 8 +- .../components/FileExplorer/FileExplorer.tsx | 18 +- .../src/components/FileExplorer/FileItem.tsx | 30 +- .../src/components/FileExplorer/FileList.tsx | 4 +- .../components/ShareDialog/ShareDialog.tsx | 4 +- .../ui/src/components/Upload/UploadZone.tsx | 6 +- hyperdrive/packages/spider/.gitignore | 35 + hyperdrive/packages/spider/Cargo.lock | 4461 ++++++++++++++++ hyperdrive/packages/spider/Cargo.toml | 10 + hyperdrive/packages/spider/metadata.json | 20 + hyperdrive/packages/spider/pkg/manifest.json | 25 + hyperdrive/packages/spider/spider/Cargo.toml | 52 + hyperdrive/packages/spider/spider/src/icon | 1 + hyperdrive/packages/spider/spider/src/lib.rs | 2332 ++++++++ .../spider/spider/src/provider/anthropic.rs | 391 ++ .../spider/spider/src/provider/mod.rs | 65 + .../spider/src/tool_providers/hypergrid.rs | 85 + .../spider/spider/src/tool_providers/mod.rs | 70 + .../packages/spider/spider/src/types.rs | 565 ++ .../packages/spider/spider/src/utils.rs | 172 + .../spider/spider/tests/integration_test.rs | 174 + hyperdrive/packages/spider/ui/index.html | 17 + .../packages/spider/ui/package-lock.json | 4750 +++++++++++++++++ hyperdrive/packages/spider/ui/package.json | 32 + .../spider/ui/public/spider-256-y.png | Bin 0 -> 68261 bytes hyperdrive/packages/spider/ui/src/App.css | 972 ++++ hyperdrive/packages/spider/ui/src/App.tsx | 107 + .../packages/spider/ui/src/auth/anthropic.ts | 85 + .../spider/ui/src/components/ApiKeys.tsx | 128 + .../spider/ui/src/components/Chat.tsx | 313 ++ .../spider/ui/src/components/ClaudeLogin.tsx | 147 + .../ui/src/components/Conversations.tsx | 57 + .../spider/ui/src/components/McpServers.tsx | 321 ++ .../spider/ui/src/components/Settings.tsx | 125 + .../spider/ui/src/components/SpiderKeys.tsx | 109 + hyperdrive/packages/spider/ui/src/index.css | 665 +++ hyperdrive/packages/spider/ui/src/main.tsx | 12 + .../spider/ui/src/services/websocket.ts | 174 + .../packages/spider/ui/src/store/spider.ts | 605 +++ .../spider/ui/src/types/caller-utils.d.ts | 16 + .../packages/spider/ui/src/types/global.ts | 32 + .../packages/spider/ui/src/types/websocket.ts | 86 + .../packages/spider/ui/src/utils/api.ts | 175 + .../packages/spider/ui/src/vite-env.d.ts | 1 + hyperdrive/packages/spider/ui/tsconfig.json | 32 + .../packages/spider/ui/tsconfig.node.json | 11 + hyperdrive/packages/spider/ui/vite.config.ts | 74 + lib/Cargo.toml | 2 +- scripts/build-packages/Cargo.toml | 2 +- scripts/build-packages/src/main.rs | 29 +- 60 files changed, 17845 insertions(+), 422 deletions(-) create mode 100644 .gitmodules create mode 160000 hyperdrive/dependencies/anthropic-api-key-manager create mode 100644 hyperdrive/packages/spider/.gitignore create mode 100644 hyperdrive/packages/spider/Cargo.lock create mode 100644 hyperdrive/packages/spider/Cargo.toml create mode 100644 hyperdrive/packages/spider/metadata.json create mode 100644 hyperdrive/packages/spider/pkg/manifest.json create mode 100644 hyperdrive/packages/spider/spider/Cargo.toml create mode 100644 hyperdrive/packages/spider/spider/src/icon create mode 100644 hyperdrive/packages/spider/spider/src/lib.rs create mode 100644 hyperdrive/packages/spider/spider/src/provider/anthropic.rs create mode 100644 hyperdrive/packages/spider/spider/src/provider/mod.rs create mode 100644 hyperdrive/packages/spider/spider/src/tool_providers/hypergrid.rs create mode 100644 hyperdrive/packages/spider/spider/src/tool_providers/mod.rs create mode 100644 hyperdrive/packages/spider/spider/src/types.rs create mode 100644 hyperdrive/packages/spider/spider/src/utils.rs create mode 100644 hyperdrive/packages/spider/spider/tests/integration_test.rs create mode 100644 hyperdrive/packages/spider/ui/index.html create mode 100644 hyperdrive/packages/spider/ui/package-lock.json create mode 100644 hyperdrive/packages/spider/ui/package.json create mode 100644 hyperdrive/packages/spider/ui/public/spider-256-y.png create mode 100644 hyperdrive/packages/spider/ui/src/App.css create mode 100644 hyperdrive/packages/spider/ui/src/App.tsx create mode 100644 hyperdrive/packages/spider/ui/src/auth/anthropic.ts create mode 100644 hyperdrive/packages/spider/ui/src/components/ApiKeys.tsx create mode 100644 hyperdrive/packages/spider/ui/src/components/Chat.tsx create mode 100644 hyperdrive/packages/spider/ui/src/components/ClaudeLogin.tsx create mode 100644 hyperdrive/packages/spider/ui/src/components/Conversations.tsx create mode 100644 hyperdrive/packages/spider/ui/src/components/McpServers.tsx create mode 100644 hyperdrive/packages/spider/ui/src/components/Settings.tsx create mode 100644 hyperdrive/packages/spider/ui/src/components/SpiderKeys.tsx create mode 100644 hyperdrive/packages/spider/ui/src/index.css create mode 100644 hyperdrive/packages/spider/ui/src/main.tsx create mode 100644 hyperdrive/packages/spider/ui/src/services/websocket.ts create mode 100644 hyperdrive/packages/spider/ui/src/store/spider.ts create mode 100644 hyperdrive/packages/spider/ui/src/types/caller-utils.d.ts create mode 100644 hyperdrive/packages/spider/ui/src/types/global.ts create mode 100644 hyperdrive/packages/spider/ui/src/types/websocket.ts create mode 100644 hyperdrive/packages/spider/ui/src/utils/api.ts create mode 100644 hyperdrive/packages/spider/ui/src/vite-env.d.ts create mode 100644 hyperdrive/packages/spider/ui/tsconfig.json create mode 100644 hyperdrive/packages/spider/ui/tsconfig.node.json create mode 100644 hyperdrive/packages/spider/ui/vite.config.ts diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..75fe446f7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "hyperdrive/dependencies/anthropic-api-key-manager"] + path = hyperdrive/dependencies/anthropic-api-key-manager + url = https://github.com/hyperware-ai/anthropic-api-key-manager diff --git a/Cargo.lock b/Cargo.lock index f1169f16d..2988e3bf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1548,6 +1548,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -2651,8 +2652,9 @@ name = "explorer" version = "0.1.0" dependencies = [ "anyhow", - "hyperprocess_macro", - "hyperware_app_common", + "file_explorer_caller_utils", + "hyperprocess_macro 0.1.0 (git+https://github.com/hyperware-ai/hyperprocess-macro?rev=66884c0)", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=4beff93)", "md5", "process_macros", "serde", @@ -2748,6 +2750,22 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "file_explorer_caller_utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "futures-util", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=4beff93)", + "once_cell", + "process_macros", + "serde", + "serde_json", + "uuid 1.17.0", + "wit-bindgen 0.41.0", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -3528,7 +3546,7 @@ dependencies = [ [[package]] name = "hyperdrive" -version = "1.6.1" +version = "1.7.0" dependencies = [ "aes-gcm", "alloy", @@ -3585,7 +3603,7 @@ dependencies = [ [[package]] name = "hyperdrive_lib" -version = "1.6.1" +version = "1.7.0" dependencies = [ "lib", ] @@ -3612,50 +3630,42 @@ dependencies = [ [[package]] name = "hyperprocess_macro" version = "0.1.0" -source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=9836e2a#9836e2abe1da7bd2ccfef837244810438af26a07" +source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=66884c0#66884c0a22b845d1db632f0fb8985a7e5bdad3fb" dependencies = [ - "anyhow", - "futures-util", - "hyperware_app_common", - "hyperware_process_lib 2.0.1", - "once_cell", - "paste", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=b9f1ead)", "proc-macro2", - "process_macros", "quote", - "rmp-serde", - "serde", - "serde_derive", - "serde_json", "syn 2.0.104", - "uuid 1.17.0", - "wit-bindgen 0.36.0", ] [[package]] -name = "hyperware_app_common" +name = "hyperprocess_macro" version = "0.1.0" -source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=9836e2a#9836e2abe1da7bd2ccfef837244810438af26a07" +source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=ed99c19#ed99c19c1e4f511786b2c827fb909ba3d3950238" dependencies = [ - "anyhow", - "futures-util", "hyperware_process_lib 2.0.1", - "once_cell", - "paste", - "process_macros", - "rmp-serde", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "hyperware-anthropic-sdk" +version = "0.1.0" +source = "git+https://github.com/hyperware-ai/hyperware-anthropic-sdk?rev=363630c#363630c261c4cc8673b92a1f2e835bab8263c866" +dependencies = [ + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=753dac3)", + "rand 0.8.5", "serde", - "serde_derive", "serde_json", - "thiserror 2.0.12", - "uuid 1.17.0", - "wit-bindgen 0.36.0", + "thiserror 1.0.69", + "url", ] [[package]] name = "hyperware_process_lib" version = "2.0.1" -source = "git+https://github.com/hyperware-ai/process_lib?rev=cfd6843#cfd6843139bc40ffeefb1304372878c07d3132b7" +source = "git+https://github.com/hyperware-ai/process_lib?rev=c27a881#c27a881e764920636ac025112533a714d622793c" dependencies = [ "alloy", "alloy-primitives", @@ -3665,6 +3675,7 @@ dependencies = [ "base64 0.22.1", "bincode", "color-eyre", + "futures-util", "http 1.3.1", "mime_guess", "rand 0.8.5", @@ -3677,6 +3688,7 @@ dependencies = [ "tracing-error", "tracing-subscriber", "url", + "uuid 1.17.0", "wit-bindgen 0.42.1", ] @@ -3711,6 +3723,96 @@ dependencies = [ "wit-bindgen 0.42.1", ] +[[package]] +name = "hyperware_process_lib" +version = "2.2.0" +source = "git+https://github.com/hyperware-ai/process_lib?rev=4beff93#4beff9389598978d2f97e4cd6d1d0f74a7acac0f" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", + "anyhow", + "base64 0.22.1", + "bincode", + "color-eyre", + "futures-util", + "http 1.3.1", + "mime_guess", + "rand 0.8.5", + "regex", + "rmp-serde", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", + "tracing-error", + "tracing-subscriber", + "url", + "uuid 1.17.0", + "wit-bindgen 0.42.1", +] + +[[package]] +name = "hyperware_process_lib" +version = "2.2.0" +source = "git+https://github.com/hyperware-ai/process_lib?rev=753dac3#753dac3d58cbf924264b47d7c0b845e2fcc03e32" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", + "anyhow", + "base64 0.22.1", + "bincode", + "color-eyre", + "futures-util", + "http 1.3.1", + "mime_guess", + "rand 0.8.5", + "regex", + "rmp-serde", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", + "tracing-error", + "tracing-subscriber", + "url", + "uuid 1.17.0", + "wit-bindgen 0.42.1", +] + +[[package]] +name = "hyperware_process_lib" +version = "2.2.0" +source = "git+https://github.com/hyperware-ai/process_lib?rev=b9f1ead#b9f1ead63356bfd4b60b337a380fef1be81d81c6" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", + "anyhow", + "base64 0.22.1", + "bincode", + "color-eyre", + "futures-util", + "http 1.3.1", + "mime_guess", + "rand 0.8.5", + "regex", + "rmp-serde", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", + "tracing-error", + "tracing-subscriber", + "url", + "uuid 1.17.0", + "wit-bindgen 0.42.1", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -4208,8 +4310,8 @@ dependencies = [ [[package]] name = "kit" -version = "3.0.1" -source = "git+https://github.com/hyperware-ai/kit?rev=79fe678#79fe678bd287bbd949e9469f4a9a5a28339ab10e" +version = "3.1.1" +source = "git+https://github.com/hyperware-ai/kit?rev=aac33b6#aac33b6f25434e12bce8c28072d216a16c7abe52" dependencies = [ "alloy", "alloy-sol-macro", @@ -4234,6 +4336,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "sha3", "syn 2.0.104", "thiserror 1.0.69", "tokio", @@ -4277,7 +4380,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lib" -version = "1.6.1" +version = "1.7.0" dependencies = [ "alloy", "anyhow", @@ -6507,13 +6610,51 @@ dependencies = [ [[package]] name = "spdx" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b69356da67e2fc1f542c71ea7e654a361a79c938e4424392ecf4fa065d2193" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" dependencies = [ "smallvec", ] +[[package]] +name = "spider" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "chrono", + "http 1.3.1", + "hyperprocess_macro 0.1.0 (git+https://github.com/hyperware-ai/hyperprocess-macro?rev=ed99c19)", + "hyperware-anthropic-sdk", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=753dac3)", + "process_macros", + "rmp-serde", + "serde", + "serde_json", + "sha2", + "spider_caller_utils", + "url", + "uuid 1.17.0", + "wit-bindgen 0.42.1", +] + +[[package]] +name = "spider_caller_utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "futures-util", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=753dac3)", + "once_cell", + "process_macros", + "serde", + "serde_json", + "uuid 1.17.0", + "wit-bindgen 0.41.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -7536,6 +7677,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde", "wasm-bindgen", ] @@ -7715,16 +7857,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e913f9242315ca39eff82aee0e19ee7a372155717ff0eb082c741e435ce25ed1" -dependencies = [ - "leb128", - "wasmparser 0.220.1", -] - [[package]] name = "wasm-encoder" version = "0.227.1" @@ -7765,22 +7897,6 @@ dependencies = [ "wasmparser 0.235.0", ] -[[package]] -name = "wasm-metadata" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185dfcd27fa5db2e6a23906b54c28199935f71d9a27a1a27b3a88d6fee2afae7" -dependencies = [ - "anyhow", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "wasm-encoder 0.220.1", - "wasmparser 0.220.1", -] - [[package]] name = "wasm-metadata" version = "0.227.1" @@ -7812,19 +7928,6 @@ dependencies = [ "wasmparser 0.230.0", ] -[[package]] -name = "wasmparser" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25" -dependencies = [ - "ahash", - "bitflags 2.9.1", - "hashbrown 0.14.5", - "indexmap", - "semver 1.0.26", -] - [[package]] name = "wasmparser" version = "0.227.1" @@ -8678,16 +8781,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "wit-bindgen" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2b3e15cd6068f233926e7d8c7c588b2ec4fb7cc7bf3824115e7c7e2a8485a3" -dependencies = [ - "wit-bindgen-rt 0.36.0", - "wit-bindgen-rust-macro 0.36.0", -] - [[package]] name = "wit-bindgen" version = "0.41.0" @@ -8708,17 +8801,6 @@ dependencies = [ "wit-bindgen-rust-macro 0.42.1", ] -[[package]] -name = "wit-bindgen-core" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b632a5a0fa2409489bd49c9e6d99fcc61bb3d4ce9d1907d44662e75a28c71172" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser 0.220.1", -] - [[package]] name = "wit-bindgen-core" version = "0.41.0" @@ -8741,15 +8823,6 @@ dependencies = [ "wit-parser 0.230.0", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7947d0131c7c9da3f01dfde0ab8bd4c4cf3c5bd49b6dba0ae640f1fa752572ea" -dependencies = [ - "bitflags 2.9.1", -] - [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -8781,22 +8854,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "wit-bindgen-rust" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4329de4186ee30e2ef30a0533f9b3c123c019a237a7c82d692807bf1b3ee2697" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.104", - "wasm-metadata 0.220.1", - "wit-bindgen-core 0.36.0", - "wit-component 0.220.1", -] - [[package]] name = "wit-bindgen-rust" version = "0.41.0" @@ -8829,21 +8886,6 @@ dependencies = [ "wit-component 0.230.0", ] -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177fb7ee1484d113b4792cc480b1ba57664bbc951b42a4beebe573502135b1fc" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.104", - "wit-bindgen-core 0.36.0", - "wit-bindgen-rust 0.36.0", -] - [[package]] name = "wit-bindgen-rust-macro" version = "0.41.0" @@ -8874,25 +8916,6 @@ dependencies = [ "wit-bindgen-rust 0.42.1", ] -[[package]] -name = "wit-component" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b505603761ed400c90ed30261f44a768317348e49f1864e82ecdc3b2744e5627" -dependencies = [ - "anyhow", - "bitflags 2.9.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.220.1", - "wasm-metadata 0.220.1", - "wasmparser 0.220.1", - "wit-parser 0.220.1", -] - [[package]] name = "wit-component" version = "0.227.1" @@ -8931,24 +8954,6 @@ dependencies = [ "wit-parser 0.230.0", ] -[[package]] -name = "wit-parser" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver 1.0.26", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.220.1", -] - [[package]] name = "wit-parser" version = "0.227.1" diff --git a/Cargo.toml b/Cargo.toml index 7a714c422..b19740ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hyperdrive_lib" authors = ["Sybil Technologies AG"] -version = "1.6.1" +version = "1.7.0" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" @@ -25,6 +25,7 @@ members = [ "hyperdrive/packages/hypermap-cacher/hypermap-cacher", "hyperdrive/packages/hypermap-cacher/reset-cache", "hyperdrive/packages/hypermap-cacher/set-nodes", "hyperdrive/packages/hypermap-cacher/start-providing", "hyperdrive/packages/hypermap-cacher/stop-providing", "hyperdrive/packages/sign/sign", + "hyperdrive/packages/spider/spider", "hyperdrive/packages/terminal/terminal", "hyperdrive/packages/terminal/add-node-provider", "hyperdrive/packages/terminal/add-rpcurl-provider", "hyperdrive/packages/terminal/alias", "hyperdrive/packages/terminal/cat", "hyperdrive/packages/terminal/clear-state", "hyperdrive/packages/terminal/echo", "hyperdrive/packages/terminal/get-providers", "hyperdrive/packages/terminal/help", "hyperdrive/packages/terminal/hfetch", "hyperdrive/packages/terminal/hi", diff --git a/README.md b/README.md index 845b43c1b..4d08bce27 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Rust must be between versions 1.81 and 1.85.1. ```bash # Clone the repo. -git clone git@github.com:hyperware-ai/hyperware.git +git clone --recurse-submodules git@github.com:hyperware-ai/hyperware.git # Install Rust and some `cargo` tools so we can build the runtime and Wasm. diff --git a/hyperdrive/Cargo.toml b/hyperdrive/Cargo.toml index 7ed06a5da..44321e69d 100644 --- a/hyperdrive/Cargo.toml +++ b/hyperdrive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hyperdrive" authors = ["Sybil Technologies AG"] -version = "1.6.1" +version = "1.7.0" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" diff --git a/hyperdrive/dependencies/anthropic-api-key-manager b/hyperdrive/dependencies/anthropic-api-key-manager new file mode 160000 index 000000000..ccfed5955 --- /dev/null +++ b/hyperdrive/dependencies/anthropic-api-key-manager @@ -0,0 +1 @@ +Subproject commit ccfed5955f2a42658aafc09a73f296fe28f3eb6b diff --git a/hyperdrive/packages-build-parameters.json b/hyperdrive/packages-build-parameters.json index ca605d406..f19ec3ddf 100644 --- a/hyperdrive/packages-build-parameters.json +++ b/hyperdrive/packages-build-parameters.json @@ -5,6 +5,18 @@ ] }, "file-explorer": { - "is_hyperapp": true + "is_hyperapp": true, + "features": [ + "caller-utils" + ] + }, + "spider": { + "is_hyperapp": true, + "local_dependencies": [ + "../dependencies/anthropic-api-key-manager" + ], + "features": [ + "caller-utils" + ] } } diff --git a/hyperdrive/packages/file-explorer/Cargo.lock b/hyperdrive/packages/file-explorer/Cargo.lock index c93b257c9..5d29c4e71 100644 --- a/hyperdrive/packages/file-explorer/Cargo.lock +++ b/hyperdrive/packages/file-explorer/Cargo.lock @@ -62,10 +62,13 @@ dependencies = [ "alloy-eips", "alloy-genesis", "alloy-json-rpc", + "alloy-network", "alloy-provider", "alloy-rpc-client", "alloy-rpc-types", "alloy-serde", + "alloy-signer", + "alloy-signer-local", "alloy-transport", "alloy-transport-http", ] @@ -436,6 +439,22 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "alloy-signer-local" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47fababf5a745133490cde927d48e50267f97d3d1209b9fc9f1d1d666964d172" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand 0.8.5", + "thiserror 2.0.12", +] + [[package]] name = "alloy-sol-macro" version = "0.8.25" @@ -897,22 +916,6 @@ dependencies = [ "serde", ] -[[package]] -name = "caller-utils" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures", - "futures-util", - "hyperware_app_common", - "once_cell", - "process_macros", - "serde", - "serde_json", - "uuid", - "wit-bindgen 0.41.0", -] - [[package]] name = "cc" version = "1.2.18" @@ -1237,9 +1240,9 @@ name = "explorer" version = "0.1.0" dependencies = [ "anyhow", - "caller-utils", + "file_explorer_caller_utils", "hyperprocess_macro", - "hyperware_app_common", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=4beff93)", "md5", "process_macros", "serde", @@ -1297,6 +1300,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "file_explorer_caller_utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "futures-util", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=4beff93)", + "once_cell", + "process_macros", + "serde", + "serde_json", + "uuid", + "wit-bindgen 0.41.0", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -1524,9 +1543,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -1668,50 +1684,48 @@ dependencies = [ [[package]] name = "hyperprocess_macro" version = "0.1.0" -source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=9836e2a#9836e2abe1da7bd2ccfef837244810438af26a07" +source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=66884c0#66884c0a22b845d1db632f0fb8985a7e5bdad3fb" dependencies = [ - "anyhow", - "futures-util", - "hyperware_app_common", - "hyperware_process_lib", - "once_cell", - "paste", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=b9f1ead)", "proc-macro2", - "process_macros", "quote", - "rmp-serde", - "serde", - "serde_derive", - "serde_json", "syn 2.0.100", - "uuid", - "wit-bindgen 0.36.0", ] [[package]] -name = "hyperware_app_common" -version = "0.1.0" -source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=9836e2a#9836e2abe1da7bd2ccfef837244810438af26a07" +name = "hyperware_process_lib" +version = "2.2.0" +source = "git+https://github.com/hyperware-ai/process_lib?rev=4beff93#4beff9389598978d2f97e4cd6d1d0f74a7acac0f" dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", "anyhow", + "base64", + "bincode", + "color-eyre", "futures-util", - "hyperware_process_lib", - "once_cell", - "paste", - "process_macros", + "http", + "mime_guess", + "rand 0.8.5", + "regex", "rmp-serde", "serde", - "serde_derive", "serde_json", - "thiserror 2.0.12", + "thiserror 1.0.69", + "tracing", + "tracing-error", + "tracing-subscriber", + "url", "uuid", - "wit-bindgen 0.36.0", + "wit-bindgen 0.42.1", ] [[package]] name = "hyperware_process_lib" -version = "2.0.1" -source = "git+https://github.com/hyperware-ai/process_lib?rev=cfd6843#cfd6843139bc40ffeefb1304372878c07d3132b7" +version = "2.2.0" +source = "git+https://github.com/hyperware-ai/process_lib?rev=b9f1ead#b9f1ead63356bfd4b60b337a380fef1be81d81c6" dependencies = [ "alloy", "alloy-primitives", @@ -1721,6 +1735,7 @@ dependencies = [ "base64", "bincode", "color-eyre", + "futures-util", "http", "mime_guess", "rand 0.8.5", @@ -1733,6 +1748,7 @@ dependencies = [ "tracing-error", "tracing-subscriber", "url", + "uuid", "wit-bindgen 0.42.1", ] @@ -1996,12 +2012,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -3099,9 +3109,9 @@ dependencies = [ [[package]] name = "spdx" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b69356da67e2fc1f542c71ea7e654a361a79c938e4424392ecf4fa065d2193" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" dependencies = [ "smallvec", ] @@ -3711,16 +3721,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e913f9242315ca39eff82aee0e19ee7a372155717ff0eb082c741e435ce25ed1" -dependencies = [ - "leb128", - "wasmparser 0.220.1", -] - [[package]] name = "wasm-encoder" version = "0.227.1" @@ -3741,22 +3741,6 @@ dependencies = [ "wasmparser 0.230.0", ] -[[package]] -name = "wasm-metadata" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185dfcd27fa5db2e6a23906b54c28199935f71d9a27a1a27b3a88d6fee2afae7" -dependencies = [ - "anyhow", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "wasm-encoder 0.220.1", - "wasmparser 0.220.1", -] - [[package]] name = "wasm-metadata" version = "0.227.1" @@ -3788,19 +3772,6 @@ dependencies = [ "wasmparser 0.230.0", ] -[[package]] -name = "wasmparser" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25" -dependencies = [ - "ahash", - "bitflags", - "hashbrown 0.14.5", - "indexmap", - "semver 1.0.26", -] - [[package]] name = "wasmparser" version = "0.227.1" @@ -4061,16 +4032,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2b3e15cd6068f233926e7d8c7c588b2ec4fb7cc7bf3824115e7c7e2a8485a3" -dependencies = [ - "wit-bindgen-rt 0.36.0", - "wit-bindgen-rust-macro 0.36.0", -] - [[package]] name = "wit-bindgen" version = "0.41.0" @@ -4091,17 +4052,6 @@ dependencies = [ "wit-bindgen-rust-macro 0.42.1", ] -[[package]] -name = "wit-bindgen-core" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b632a5a0fa2409489bd49c9e6d99fcc61bb3d4ce9d1907d44662e75a28c71172" -dependencies = [ - "anyhow", - "heck", - "wit-parser 0.220.1", -] - [[package]] name = "wit-bindgen-core" version = "0.41.0" @@ -4124,15 +4074,6 @@ dependencies = [ "wit-parser 0.230.0", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7947d0131c7c9da3f01dfde0ab8bd4c4cf3c5bd49b6dba0ae640f1fa752572ea" -dependencies = [ - "bitflags", -] - [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -4164,22 +4105,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "wit-bindgen-rust" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4329de4186ee30e2ef30a0533f9b3c123c019a237a7c82d692807bf1b3ee2697" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.100", - "wasm-metadata 0.220.1", - "wit-bindgen-core 0.36.0", - "wit-component 0.220.1", -] - [[package]] name = "wit-bindgen-rust" version = "0.41.0" @@ -4212,21 +4137,6 @@ dependencies = [ "wit-component 0.230.0", ] -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177fb7ee1484d113b4792cc480b1ba57664bbc951b42a4beebe573502135b1fc" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.100", - "wit-bindgen-core 0.36.0", - "wit-bindgen-rust 0.36.0", -] - [[package]] name = "wit-bindgen-rust-macro" version = "0.41.0" @@ -4257,25 +4167,6 @@ dependencies = [ "wit-bindgen-rust 0.42.1", ] -[[package]] -name = "wit-component" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b505603761ed400c90ed30261f44a768317348e49f1864e82ecdc3b2744e5627" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.220.1", - "wasm-metadata 0.220.1", - "wasmparser 0.220.1", - "wit-parser 0.220.1", -] - [[package]] name = "wit-component" version = "0.227.1" @@ -4314,24 +4205,6 @@ dependencies = [ "wit-parser 0.230.0", ] -[[package]] -name = "wit-parser" -version = "0.220.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver 1.0.26", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.220.1", -] - [[package]] name = "wit-parser" version = "0.227.1" diff --git a/hyperdrive/packages/file-explorer/explorer/Cargo.toml b/hyperdrive/packages/file-explorer/explorer/Cargo.toml index 3b36f1446..884c9deeb 100644 --- a/hyperdrive/packages/file-explorer/explorer/Cargo.toml +++ b/hyperdrive/packages/file-explorer/explorer/Cargo.toml @@ -9,11 +9,12 @@ wit-bindgen = "0.42.1" [dependencies.hyperprocess_macro] git = "https://github.com/hyperware-ai/hyperprocess-macro" -rev = "9836e2a" +rev = "66884c0" -[dependencies.hyperware_app_common] -git = "https://github.com/hyperware-ai/hyperprocess-macro" -rev = "9836e2a" +[dependencies.hyperware_process_lib] +features = ["hyperapp"] +git = "https://github.com/hyperware-ai/process_lib" +rev = "4beff93" [dependencies.serde] features = ["derive"] diff --git a/hyperdrive/packages/file-explorer/explorer/src/lib.rs b/hyperdrive/packages/file-explorer/explorer/src/lib.rs index db07d1061..5aabd4943 100644 --- a/hyperdrive/packages/file-explorer/explorer/src/lib.rs +++ b/hyperdrive/packages/file-explorer/explorer/src/lib.rs @@ -1,12 +1,12 @@ use hyperprocess_macro::hyperprocess; -use hyperware_app_common::hyperware_process_lib::logging::{ +use hyperware_process_lib::logging::{ debug, error, info, init_logging, Level, }; -use hyperware_app_common::hyperware_process_lib::our; -use hyperware_app_common::hyperware_process_lib::vfs::{ +use hyperware_process_lib::our; +use hyperware_process_lib::vfs::{ self, create_drive, vfs_request, FileType, VfsAction, VfsResponse, }; -use hyperware_app_common::{send, SaveOptions}; +use hyperware_process_lib::hyperapp::{add_response_header, get_path, send, SaveOptions}; use std::collections::HashMap; const ICON: &str = include_str!("./icon"); @@ -179,6 +179,7 @@ impl FileExplorerState { let vfs_path = path.clone(); vfs::remove_file(&vfs_path, Some(5)) + .await .map_err(|e| format!("Failed to delete file: {}", e))?; Ok(true) @@ -267,7 +268,7 @@ impl FileExplorerState { #[http] async fn serve_shared_file(&mut self) -> Result, String> { // Use get_path() to handle routing - let request_path = hyperware_app_common::get_path(); + let request_path = get_path(); // Extract the file path from the request if let Some(request_path_str) = request_path { @@ -281,7 +282,7 @@ impl FileExplorerState { let filename = path.split('/').last().unwrap_or("download"); // Set Content-Disposition header to preserve original filename - hyperware_app_common::add_response_header( + add_response_header( "Content-Disposition".to_string(), format!("attachment; filename=\"{}\"", filename), ); @@ -300,7 +301,7 @@ impl FileExplorerState { Some("zip") => "application/zip", _ => "application/octet-stream", }; - hyperware_app_common::add_response_header( + add_response_header( "Content-Type".to_string(), content_type.to_string(), ); @@ -462,7 +463,7 @@ async fn list_directory_contents(path: &str) -> Result, String> { }); } else { // For files, try to get metadata - if let Ok(meta) = vfs::metadata(&sub_full_path, Some(5)) { + if let Ok(meta) = vfs::metadata(&sub_full_path, Some(5)).await { all_files.push(FileInfo { name: sub_filename, path: sub_full_path, @@ -479,6 +480,7 @@ async fn list_directory_contents(path: &str) -> Result, String> { } else { // For files, get metadata let meta = vfs::metadata(&full_path, Some(5)) + .await .map_err(|e| format!("Failed to get metadata for '{}': {}", entry.path, e))?; all_files.push(FileInfo { diff --git a/hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx b/hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx index f612b203f..c870ee11c 100644 --- a/hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { FileInfo, unshareFile, deleteFile, deleteDirectory } from '../../lib/api'; +import { FileInfo, unshare_file, delete_file, delete_directory } from '../../lib/api'; import useFileExplorerStore from '../../store/fileExplorer'; import './ContextMenu.css'; @@ -15,7 +15,7 @@ interface ContextMenuProps { const ContextMenu: React.FC = ({ position, file, onClose, onShare, onDelete, openedByTouch }) => { const menuRef = useRef(null); const { isFileShared, removeSharedLink } = useFileExplorerStore(); - const isShared = !file.isDirectory && isFileShared(file.path); + const isShared = !file.is_directory && isFileShared(file.path); // Track if a new touch has started after menu opened const touchStartedRef = useRef(false); @@ -84,7 +84,7 @@ const ContextMenu: React.FC = ({ position, file, onClose, onSh const handleUnshare = async () => { try { - await unshareFile(file.path); + await unshare_file(file.path); removeSharedLink(file.path); onClose(); } catch (err) { @@ -108,7 +108,7 @@ const ContextMenu: React.FC = ({ position, file, onClose, onSh - {!file.isDirectory && ( + {!file.is_directory && ( isShared ? ( + + + + + + + + +
+ {activeTab === 'chat' && } + {activeTab === 'api-keys' && } + {activeTab === 'spider-keys' && } + {activeTab === 'mcp-servers' && } + {activeTab === 'conversations' && } + {activeTab === 'settings' && } +
+ + ); +} + +export default App; \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/auth/anthropic.ts b/hyperdrive/packages/spider/ui/src/auth/anthropic.ts new file mode 100644 index 000000000..5b71b0af4 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/auth/anthropic.ts @@ -0,0 +1,85 @@ +import { ApiError, exchange_oauth_token as exchangeOauthToken, refresh_oauth_token as refreshOauthToken } from '@caller-utils'; + +export namespace AuthAnthropic { + const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; + + // Generate PKCE challenge and verifier + async function generatePKCE() { + const generateRandomString = (length: number) => { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return btoa(String.fromCharCode(...array)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + }; + + const verifier = generateRandomString(32); + + const encoder = new TextEncoder(); + const data = encoder.encode(verifier); + const hash = await crypto.subtle.digest('SHA-256', data); + + const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + + return { verifier, challenge }; + } + + export async function authorize() { + const pkce = await generatePKCE(); + + const url = new URL("https://claude.ai/oauth/authorize"); + url.searchParams.set("code", "true"); + url.searchParams.set("client_id", CLIENT_ID); + url.searchParams.set("response_type", "code"); + url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback"); + url.searchParams.set("scope", "org:create_api_key user:profile user:inference"); + url.searchParams.set("code_challenge", pkce.challenge); + url.searchParams.set("code_challenge_method", "S256"); + url.searchParams.set("state", pkce.verifier); + + return { + url: url.toString(), + verifier: pkce.verifier, + }; + } + + export async function exchange(code: string, verifier: string) { + try { + // Use the backend proxy to avoid CORS issues + const result = await exchangeOauthToken({ + code: code, + verifier: verifier, + }); + + return result; + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + throw new ExchangeFailed(); + } + } + + export async function refresh(refreshToken: string) { + try { + // Use the backend proxy to avoid CORS issues + const result = await refreshOauthToken({ + refreshToken: refreshToken, + }); + + return result; + } catch (error) { + throw new Error("Failed to refresh token"); + } + } + + export class ExchangeFailed extends Error { + constructor() { + super("Exchange failed"); + } + } +} diff --git a/hyperdrive/packages/spider/ui/src/components/ApiKeys.tsx b/hyperdrive/packages/spider/ui/src/components/ApiKeys.tsx new file mode 100644 index 000000000..b50b1699b --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/ApiKeys.tsx @@ -0,0 +1,128 @@ +import { useState } from 'react'; +import { useSpiderStore } from '../store/spider'; +import { ClaudeLogin } from './ClaudeLogin'; + +export default function ApiKeys() { + const { apiKeys, isLoading, error, setApiKey, removeApiKey } = useSpiderStore(); + const [showAddForm, setShowAddForm] = useState(false); + const [showClaudeLogin, setShowClaudeLogin] = useState(false); + const [provider, setProvider] = useState('anthropic'); + const [apiKeyValue, setApiKeyValue] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!apiKeyValue.trim()) return; + + await setApiKey(provider, apiKeyValue); + setApiKeyValue(''); + setShowAddForm(false); + }; + + return ( +
+
+

API Keys

+
+ + +
+
+ + {error && ( +
+ {error} +
+ )} + + {showClaudeLogin && ( +
+ { + setShowClaudeLogin(false); + // Refresh API keys list + useSpiderStore.getState().loadApiKeys(); + }} + onCancel={() => setShowClaudeLogin(false)} + /> +
+ )} + + {showAddForm && ( +
+
+ + +
+ +
+ + setApiKeyValue(e.target.value)} + placeholder="sk-..." + required + /> +
+ + +
+ )} + +
+
+ {apiKeys.length === 0 ? ( +

No API keys configured

+ ) : ( + apiKeys.map((key) => ( +
+
+

{key.provider}

+

Key: {key.keyPreview}

+

Created: {new Date(key.createdAt * 1000).toLocaleDateString()}

+ {key.lastUsed && ( +

Last used: {new Date(key.lastUsed * 1000).toLocaleDateString()}

+ )} +
+ +
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/Chat.tsx b/hyperdrive/packages/spider/ui/src/components/Chat.tsx new file mode 100644 index 000000000..6c1ad09d4 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/Chat.tsx @@ -0,0 +1,313 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useSpiderStore } from '../store/spider'; +import ReactMarkdown from 'react-markdown'; +import { webSocketService } from '../services/websocket'; + +interface ToolCall { + id: string; + tool_name: string; + parameters: string; +} + +interface ToolResult { + tool_call_id: string; + result: string; +} + +function ToolCallModal({ toolCall, toolResult, onClose }: { + toolCall: ToolCall; + toolResult?: ToolResult; + onClose: () => void; +}) { + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text).then(() => { + // Could add a toast notification here + }).catch(err => { + console.error('Failed to copy:', err); + }); + }; + + return ( +
+
e.stopPropagation()}> +
+

Tool Call Details: {toolCall.tool_name}

+ +
+
+
+
+

Tool Call

+ +
+
+              {JSON.stringify(toolCall, null, 2)}
+            
+
+ {toolResult && ( +
+
+

Tool Result

+ +
+
+                {JSON.stringify(toolResult, null, 2)}
+              
+
+ )} +
+
+
+ ); +} + +export default function Chat() { + const { + activeConversation, + isLoading, + error, + sendMessage, + clearActiveConversation, + cancelRequest, + wsConnected, + useWebSocket + } = useSpiderStore(); + const [message, setMessage] = useState(''); + const [selectedToolCall, setSelectedToolCall] = useState<{call: ToolCall, result?: ToolResult} | null>(null); + const abortControllerRef = useRef(null); + const messagesEndRef = useRef(null); + const chatMessagesRef = useRef(null); + const inputRef = useRef(null); + + // Auto-scroll to bottom when new messages arrive + const scrollToBottom = (smooth: boolean = true) => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'end' }); + } + }; + + // Scroll on new messages or loading state changes + useEffect(() => { + // Use a small delay to ensure DOM has updated + const timer = setTimeout(() => { + scrollToBottom(); + }, 100); + return () => clearTimeout(timer); + }, [activeConversation?.messages?.length, isLoading]); + + // Scroll immediately when conversation changes + useEffect(() => { + scrollToBottom(false); + }, [activeConversation?.id]); + + // Auto-focus input when loading completes (assistant finishes responding) + useEffect(() => { + if (!isLoading && inputRef.current) { + inputRef.current.focus(); + } + }, [isLoading]); + + // Helper to get tool emoji - always use the same emoji + const getToolEmoji = (toolName: string) => { + return '🔧'; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!message.trim() || isLoading) return; + + const controller = new AbortController(); + abortControllerRef.current = controller; + + try { + await sendMessage(message, controller.signal); + setMessage(''); + // Scroll after sending message + setTimeout(() => scrollToBottom(), 50); + } catch (err: any) { + if (err.name !== 'AbortError') { + console.error('Failed to send message:', err); + } + } finally { + abortControllerRef.current = null; + } + }; + + const handleCancel = () => { + // Cancel HTTP request if using HTTP + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + + // Cancel WebSocket request if using WebSocket + if (useWebSocket && wsConnected) { + webSocketService.sendCancel(); + } + + // Update store state + if (cancelRequest) { + cancelRequest(); + } + }; + + const handleNewConversation = () => { + clearActiveConversation(); + }; + + return ( +
+
+

Chat

+
+ {useWebSocket && ( + + {wsConnected ? '🟢' : '🔴'} {wsConnected ? 'Live' : 'HTTP'} + + )} + +
+
+ + {error && ( +
+ {error} +
+ )} + +
+ {activeConversation?.messages.map((msg, index) => { + const toolCalls = msg.toolCallsJson ? JSON.parse(msg.toolCallsJson) as ToolCall[] : null; + const toolResults = msg.toolResultsJson ? JSON.parse(msg.toolResultsJson) as ToolResult[] : null; + const nextMsg = activeConversation.messages[index + 1]; + const hasToolResult = nextMsg?.role === 'tool' && nextMsg.toolResultsJson; + + return ( + + {msg.role !== 'tool' && (msg.content && msg.content.trim() && msg.content !== '[Tool calls pending]') && ( +
+
+ {msg.content} +
+
+ )} + + {/* Display tool calls as separate message-like items */} + {toolCalls && toolCalls.map((toolCall, toolIndex) => { + // Find the corresponding tool result in the next message if it's a tool message + const toolResultFromNext = nextMsg?.role === 'tool' && nextMsg.toolResultsJson + ? (JSON.parse(nextMsg.toolResultsJson) as ToolResult[])?.find(r => r.tool_call_id === toolCall.id) + : null; + + const isLastMessage = index === activeConversation.messages.length - 1; + const isWaitingForResult = isLastMessage && isLoading && !toolResultFromNext; + + return ( +
+ {getToolEmoji(toolCall.tool_name)} + {isWaitingForResult ? ( + <> + {toolCall.tool_name} + + + ) : ( + + )} +
+ ); + })} +
+ ); + }) || ( +
+

Start a conversation by typing a message below

+
+ )} + {isLoading && activeConversation && ( +
+
+
+ + Thinking... +
+
+
+ )} +
+
+ + {selectedToolCall && ( + setSelectedToolCall(null)} + /> + )} + +
+ setMessage(e.target.value)} + placeholder={isLoading ? "Thinking..." : "Type your message..."} + disabled={isLoading} + className={`chat-input ${isLoading ? 'chat-input-thinking' : ''}`} + /> + {isLoading ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/hyperdrive/packages/spider/ui/src/components/ClaudeLogin.tsx b/hyperdrive/packages/spider/ui/src/components/ClaudeLogin.tsx new file mode 100644 index 000000000..420fced48 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/ClaudeLogin.tsx @@ -0,0 +1,147 @@ +import React, { useState } from 'react'; +import { AuthAnthropic } from '../auth/anthropic'; +import { useSpiderStore } from '../store/spider'; + +interface ClaudeLoginProps { + onSuccess?: () => void; + onCancel?: () => void; +} + +export const ClaudeLogin: React.FC = ({ onSuccess, onCancel }) => { + const [isLoading, setIsLoading] = useState(false); + const [authUrl, setAuthUrl] = useState(null); + const [verifier, setVerifier] = useState(null); + const [authCode, setAuthCode] = useState(''); + const [error, setError] = useState(null); + + const handleStartAuth = async () => { + setIsLoading(true); + setError(null); + + try { + const { url, verifier: v } = await AuthAnthropic.authorize(); + setAuthUrl(url); + setVerifier(v); + + // Open auth URL in new window + window.open(url, '_blank', 'width=600,height=700'); + } catch (err) { + setError('Failed to start authentication'); + console.error(err); + } finally { + setIsLoading(false); + } + }; + + const handleExchangeCode = async () => { + if (!authCode || !verifier) { + setError('Please enter the authorization code'); + return; + } + + setIsLoading(true); + setError(null); + + try { + const tokens = await AuthAnthropic.exchange(authCode, verifier); + + // Store tokens in localStorage + localStorage.setItem('claude_oauth', JSON.stringify({ + refresh: tokens.refresh, + access: tokens.access, + expires: tokens.expires, + })); + + // Store as API key in Spider + const store = useSpiderStore.getState(); + await store.setApiKey('anthropic-oauth', tokens.access); + + onSuccess?.(); + } catch (err) { + setError('Invalid authorization code. Please try again.'); + console.error(err); + } finally { + setIsLoading(false); + } + }; + + + return ( +
+
+
+

Login with Claude

+

+ Authenticate with your Claude.ai account to use your subscription for chat. +

+
+ + + {!authUrl ? ( +
+ +
+ ) : ( +
+
+

+ A new window has opened for authentication. After you authorize the app, + you'll receive a code. Please paste it below: +

+ + Click here if the window didn't open + +
+ +
+ + setAuthCode(e.target.value)} + placeholder="Paste the code here" + className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + +
+ )} + + {error && ( +
+ {error} +
+ )} + + {onCancel && ( + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/Conversations.tsx b/hyperdrive/packages/spider/ui/src/components/Conversations.tsx new file mode 100644 index 000000000..d58baf443 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/Conversations.tsx @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; +import { useSpiderStore } from '../store/spider'; + +export default function Conversations() { + const { conversations, loadConversations, loadConversation, isLoading } = useSpiderStore(); + + useEffect(() => { + loadConversations(); + }, [loadConversations]); + + const handleSelectConversation = async (id: string) => { + await loadConversation(id); + // Switch to Chat tab after loading conversation + if ((window as any).switchToChat) { + (window as any).switchToChat(); + } + }; + + return ( +
+
+

Conversation History

+ +
+ +
+
+ {conversations.length === 0 ? ( +

No conversations yet

+ ) : ( + conversations.map((conv) => ( +
handleSelectConversation(conv.id)} + > +
+

Conversation {conv.id.substring(0, 8)}...

+

Client: {conv.metadata.client}

+

Started: {conv.metadata.start_time}

+

Messages: {conv.messages.length}

+

Provider: {conv.llm_provider}

+
+
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/McpServers.tsx b/hyperdrive/packages/spider/ui/src/components/McpServers.tsx new file mode 100644 index 000000000..b3990be06 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/McpServers.tsx @@ -0,0 +1,321 @@ +import { useState, useEffect } from 'react'; +import { useSpiderStore } from '../store/spider'; + +export default function McpServers() { + const { + mcpServers, + isLoading, + error, + addMcpServer, + connectMcpServer, + disconnectMcpServer, + removeMcpServer, + loadMcpServers + } = useSpiderStore(); + const [showAddForm, setShowAddForm] = useState(false); + const [serverName, setServerName] = useState(''); + const [transportType, setTransportType] = useState('websocket'); + const [url, setUrl] = useState('ws://localhost:10125'); + const [hypergridUrl, setHypergridUrl] = useState('http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp'); + const [hypergridToken, setHypergridToken] = useState(''); + const [hypergridClientId, setHypergridClientId] = useState(''); + const [hypergridNode, setHypergridNode] = useState(''); + const [connectingServers, setConnectingServers] = useState>(new Set()); + + // Periodically refresh server status + useEffect(() => { + const interval = setInterval(() => { + loadMcpServers(); + }, 5000); // Refresh every 5 seconds + + return () => clearInterval(interval); + }, [loadMcpServers]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + let transport: any = { + transportType: transportType, + command: null, + args: null, + url: null, + hypergridToken: null, + hypergridClientId: null, + hypergridNode: null + }; + + if (transportType === 'websocket') { + transport.url = url; + } else if (transportType === 'hypergrid') { + transport.url = hypergridUrl; + transport.hypergridToken = hypergridToken; + transport.hypergridClientId = hypergridClientId; + transport.hypergridNode = hypergridNode; + } + + await addMcpServer(serverName, transport); + setServerName(''); + setTransportType('websocket'); + setUrl('ws://localhost:10125'); + setHypergridUrl('http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp'); + setHypergridToken(''); + setHypergridClientId(''); + setHypergridNode(''); + setShowAddForm(false); + + // Refresh servers list after adding + setTimeout(() => loadMcpServers(), 500); + }; + + const handleConnect = async (serverId: string) => { + setConnectingServers(prev => new Set(prev).add(serverId)); + try { + await connectMcpServer(serverId); + // Poll for connection status update + let attempts = 0; + const pollInterval = setInterval(async () => { + await loadMcpServers(); + attempts++; + const server = mcpServers.find(s => s.id === serverId); + if (server?.connected || attempts > 10) { + clearInterval(pollInterval); + setConnectingServers(prev => { + const next = new Set(prev); + next.delete(serverId); + return next; + }); + } + }, 500); + } catch (error) { + setConnectingServers(prev => { + const next = new Set(prev); + next.delete(serverId); + return next; + }); + } + }; + + const handleDisconnect = async (serverId: string) => { + await disconnectMcpServer(serverId); + // Refresh servers list after disconnecting + setTimeout(() => loadMcpServers(), 500); + }; + + const handleRemove = async (serverId: string) => { + if (confirm('Are you sure you want to remove this MCP server?')) { + await removeMcpServer(serverId); + // Refresh servers list after removing + setTimeout(() => loadMcpServers(), 500); + } + }; + + return ( +
+
+

MCP Servers

+ +
+ + {error && ( +
+ {error} +
+ )} + + {showAddForm && ( +
+
+ + setServerName(e.target.value)} + placeholder="My MCP Server" + required + /> +
+ +
+ + +
+ + {transportType === 'websocket' && ( +
+ + setUrl(e.target.value)} + placeholder="ws://localhost:10125" + required + /> + + URL of the WebSocket MCP server or ws-mcp wrapper + +
+ )} + + {transportType === 'hypergrid' && ( + <> +
+ + setHypergridUrl(e.target.value)} + placeholder="http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp" + required + /> + + Base URL for the Hypergrid API endpoint + +
+ +
+ + setHypergridToken(e.target.value)} + placeholder="Enter your hypergrid token (optional for initial connection)" + /> + + Token for authenticating with the Hypergrid network + +
+ +
+ + setHypergridClientId(e.target.value)} + placeholder="Enter your client ID" + required + /> + + Unique identifier for this client + +
+ +
+ + setHypergridNode(e.target.value)} + placeholder="Enter your Hyperware node name" + required + /> + + Name of your Hyperware node + +
+ + )} + + +
+ )} + +
+
+ {mcpServers.length === 0 ? ( +

No MCP servers configured

+ ) : ( + mcpServers.map((server) => { + const isConnecting = connectingServers.has(server.id); + return ( +
+
+

{server.name}

+

+ Status: { + isConnecting ? '🟡 Connecting...' : + server.connected ? '🟢 Connected' : + '🔴 Disconnected' + } +

+

+ Transport: {server.transport.transportType === 'hypergrid' ? + `Hypergrid - ${server.transport.hypergridNode || 'Not configured'}` : + `WebSocket - ${server.transport.url || 'No URL specified'}` + } +

+

Tools: {server.tools.length}

+ {server.tools.length > 0 && ( +
+ Available Tools +
    + {server.tools.map((tool, index) => ( +
  • + {tool.name}: {tool.description} + {tool.inputSchemaJson && (✓ Schema)} +
  • + ))} +
+
+ )} +
+
+ {!server.connected && !isConnecting && ( + + )} + {server.connected && ( + + )} + +
+
+ ); + }) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/Settings.tsx b/hyperdrive/packages/spider/ui/src/components/Settings.tsx new file mode 100644 index 000000000..a08a4d019 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/Settings.tsx @@ -0,0 +1,125 @@ +import { useState, useEffect } from 'react'; +import { useSpiderStore } from '../store/spider'; + +export default function Settings() { + const { config, isLoading, error, updateConfig } = useSpiderStore(); + const [provider, setProvider] = useState(config.defaultLlmProvider); + const [model, setModel] = useState(''); + const [maxTokens, setMaxTokens] = useState(config.maxTokens); + const [temperature, setTemperature] = useState(config.temperature); + + // Model options based on provider + const modelOptions = { + anthropic: [ + { value: 'claude-opus-4-1-20250805', label: 'Claude 4.1 Opus' }, + { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet' } + ], + openai: [ + { value: 'gpt-4o', label: 'GPT-4o' }, + { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, + { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' }, + { value: 'gpt-4', label: 'GPT-4' }, + { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo' } + ], + google: [ + { value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash (Experimental)' }, + { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' }, + { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }, + { value: 'gemini-pro', label: 'Gemini Pro' } + ] + }; + + useEffect(() => { + setProvider(config.defaultLlmProvider); + setMaxTokens(config.maxTokens); + setTemperature(config.temperature); + }, [config]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await updateConfig({ + defaultLlmProvider: provider, + maxTokens: maxTokens, + temperature: temperature, + }); + }; + + return ( +
+
+

Settings

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+
+ + +
+ +
+ + +
+
+ +
+ + setMaxTokens(Number(e.target.value))} + min="1" + max="100000" + /> +
+ +
+ + setTemperature(Number(e.target.value))} + min="0" + max="2" + step="0.1" + /> +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/SpiderKeys.tsx b/hyperdrive/packages/spider/ui/src/components/SpiderKeys.tsx new file mode 100644 index 000000000..bd414e732 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/SpiderKeys.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import { useSpiderStore } from '../store/spider'; + +export default function SpiderKeys() { + const { spiderKeys, isLoading, error, createSpiderKey, revokeSpiderKey } = useSpiderStore(); + const [showAddForm, setShowAddForm] = useState(false); + const [keyName, setKeyName] = useState(''); + const [permissions, setPermissions] = useState(['read']); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!keyName.trim()) return; + + await createSpiderKey(keyName, permissions); + setKeyName(''); + setPermissions(['read']); + setShowAddForm(false); + }; + + const togglePermission = (perm: string) => { + setPermissions(prev => + prev.includes(perm) + ? prev.filter(p => p !== perm) + : [...prev, perm] + ); + }; + + return ( +
+
+

Spider API Keys

+ +
+ + {error && ( +
+ {error} +
+ )} + + {showAddForm && ( +
+
+ + setKeyName(e.target.value)} + placeholder="My API Key" + required + /> +
+ +
+ +
+ {['read', 'write', 'chat', 'admin'].map(perm => ( + + ))} +
+
+ + +
+ )} + +
+
+ {spiderKeys.length === 0 ? ( +

No Spider API keys generated

+ ) : ( + spiderKeys.map((key) => ( +
+
+

{key.name}

+

Key: {key.key}

+

Permissions: {key.permissions.join(', ')}

+

Created: {new Date(key.createdAt * 1000).toLocaleDateString()}

+
+ +
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/index.css b/hyperdrive/packages/spider/ui/src/index.css new file mode 100644 index 000000000..baa2da425 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/index.css @@ -0,0 +1,665 @@ +/* Root CSS for Skeleton App */ + +:root { + /* Color scheme - customize these for your app */ + --primary-color: #646cff; + --primary-hover: #535bf2; + --background: #242424; + --surface: #1a1a1a; + --text-primary: rgba(255, 255, 255, 0.87); + --text-secondary: rgba(255, 255, 255, 0.6); + --border-color: rgba(255, 255, 255, 0.1); + --error-color: #ff6b6b; + --success-color: #51cf66; + + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: dark; + color: var(--text-primary); + background-color: var(--background); + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + min-width: 320px; + min-height: 100vh; + overflow: hidden; +} + +#root { + width: 100%; + height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: var(--surface); + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: var(--primary-color); +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +input, textarea { + padding: 0.6em; + font-size: 1em; + border-radius: 4px; + border: 1px solid var(--border-color); + background-color: var(--surface); + color: var(--text-primary); + font-family: inherit; +} + +input:focus, textarea:focus { + outline: none; + border-color: var(--primary-color); +} + +/* Utility classes */ +.error { + color: var(--error-color); + padding: 1em; + border-radius: 4px; + background-color: rgba(255, 107, 107, 0.1); + border: 1px solid var(--error-color); +} + +.success { + color: var(--success-color); + padding: 1em; + border-radius: 4px; + background-color: rgba(81, 207, 102, 0.1); + border: 1px solid var(--success-color); +} + +.loading { + opacity: 0.6; + pointer-events: none; +} + +/* Chat Styles */ +.chat-container { + display: flex; + flex-direction: column; + height: 600px; + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; +} + +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--border-color); + background-color: var(--surface); +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.message { + padding: 0.15rem 0.75rem; + border-radius: 8px; + max-width: 80%; + margin-bottom: 0.05rem; +} + +.message-user { + align-self: flex-end; + background-color: var(--primary-color); + color: white; +} + +.message-assistant { + align-self: flex-start; + background-color: var(--surface); + border: 1px solid var(--border-color); +} + +.message-thinking { + opacity: 0.7; +} + +.message-role { + font-size: 0.8rem; + font-weight: bold; + margin-bottom: 0.5rem; + text-transform: capitalize; +} + +.message-content { + line-height: 1.25; +} + +.message-content h1, +.message-content h2, +.message-content h3, +.message-content h4, +.message-content h5, +.message-content h6 { + margin-top: 0.5rem; + margin-bottom: 0.25rem; +} + +.message-content p { + margin: 0.25rem 0; +} + +.message-content ul, +.message-content ol { + margin: 0.25rem 0; + padding-left: 1.5rem; +} + +.message-content code { + background-color: rgba(255, 255, 255, 0.1); + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: 'Courier New', monospace; +} + +.message-content pre { + background-color: rgba(0, 0, 0, 0.2); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; +} + +.message-content pre code { + background-color: transparent; + padding: 0; +} + +.thinking-indicator { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.chat-input-form { + display: flex; + gap: 0.5rem; + padding: 1rem; + border-top: 1px solid var(--border-color); + background-color: var(--surface); +} + +.chat-input { + flex: 1; +} + +.chat-input-thinking { + opacity: 0.6; +} + +.empty-chat { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + color: var(--text-secondary); +} + +/* Button Styles */ +.btn { + padding: 0.5rem 1rem; + border-radius: 4px; + border: none; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background-color: var(--primary-hover); +} + +.btn-danger { + background-color: var(--error-color); + color: white; +} + +.btn-danger:hover:not(:disabled) { + background-color: #ff5252; +} + +.btn-success { + background-color: var(--success-color); + color: white; +} + +.btn-success:hover:not(:disabled) { + background-color: #45b55a; +} + +.btn-warning { + background-color: #ffa94d; + color: #1a1a1a; +} + +.btn-warning:hover:not(:disabled) { + background-color: #ff922b; +} + +.btn-icon { + padding: 0.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 36px; + height: 36px; +} + +.new-conversation-btn { + background-color: transparent; + border: 1px solid var(--border-color); +} + +.new-conversation-btn:hover { + background-color: var(--surface); +} + +/* MCP Server Styles */ +.component-container { + padding: 1rem; +} + +.component-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.mcp-server-form { + background-color: var(--surface); + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.form-group input { + width: 100%; +} + +.form-help { + display: block; + margin-top: 0.25rem; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.transport-info { + padding: 0.5rem; + background-color: var(--background); + border-radius: 4px; + color: var(--text-secondary); +} + +.mcp-servers-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.mcp-server-item { + display: flex; + justify-content: space-between; + align-items: start; + padding: 1rem; + background-color: var(--surface); + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.mcp-server-info { + flex: 1; +} + +.mcp-server-info h3 { + margin: 0 0 0.5rem 0; +} + +.mcp-server-info p { + margin: 0.25rem 0; + color: var(--text-secondary); +} + +.mcp-server-actions { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.mcp-server-tools { + margin-top: 0.5rem; +} + +.mcp-server-tools summary { + cursor: pointer; + font-weight: 500; + color: var(--primary-color); +} + +.mcp-server-tools ul { + margin: 0.5rem 0 0 1rem; + padding: 0; + list-style: none; +} + +.mcp-server-tools li { + margin: 0.25rem 0; + font-size: 0.9rem; +} + +.error-message { + color: var(--error-color); + padding: 0.75rem; + border-radius: 4px; + background-color: rgba(255, 107, 107, 0.1); + border: 1px solid var(--error-color); + margin-bottom: 1rem; +} + +.empty-state { + text-align: center; + color: var(--text-secondary); + padding: 2rem; +} + +/* Tool calls and results */ +.tool-calls, +.tool-results { + margin-top: 0.5rem; + padding: 0.5rem; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +.tool-calls summary, +.tool-results summary { + cursor: pointer; + font-weight: 500; + color: var(--text-secondary); +} + +.tool-calls pre, +.tool-results pre { + margin: 0.5rem 0 0 0; + padding: 0.5rem; + background-color: rgba(0, 0, 0, 0.3); + border-radius: 3px; + overflow-x: auto; + font-size: 0.85rem; +} + +/* Tool message styles */ +.message-tool { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.15rem 0.75rem; + background-color: var(--surface); + border: 1px solid var(--border-color); + border-radius: 8px; + align-self: flex-start; + max-width: fit-content; +} + +.tool-emoji { + font-size: 1.2rem; +} + +.tool-name { + color: var(--text-primary); +} + +.tool-spinner { + margin-left: 0.5rem; +} + +.tool-link { + background: none; + border: none; + color: var(--primary-color); + cursor: pointer; + padding: 0; + font-size: 1rem; + text-decoration: underline; + transition: opacity 0.2s; +} + +.tool-link:hover { + opacity: 0.8; +} + +/* Modal styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background-color: var(--surface); + border-radius: 8px; + max-width: 600px; + max-height: 80vh; + width: 90%; + display: flex; + flex-direction: column; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + margin: 0; +} + +.modal-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.5rem; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-close:hover { + color: var(--text-primary); +} + +.modal-body { + padding: 1rem; + overflow-y: auto; + flex: 1; +} + +.modal-section { + margin-bottom: 1.5rem; +} + +.modal-section:last-child { + margin-bottom: 0; +} + +.modal-section h4 { + margin: 0 0 0.5rem 0; + color: var(--text-secondary); +} + +.json-display { + background-color: rgba(0, 0, 0, 0.2); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + line-height: 1.4; + color: var(--text-primary); +} + +.copy-btn { + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-secondary); + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + transition: all 0.2s; +} + +.copy-btn:hover { + color: var(--text-primary); + border-color: var(--primary-color); + background: var(--surface); +} + +/* WebSocket status indicator */ +.ws-status { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 500; +} + +.ws-connected { + background-color: rgba(81, 207, 102, 0.1); + color: var(--success-color); +} + +.ws-disconnected { + background-color: rgba(255, 107, 107, 0.1); + color: var(--error-color); +} + +/* Light mode support */ +@media (prefers-color-scheme: light) { + :root { + --background: #ffffff; + --surface: #f5f5f5; + --text-primary: #213547; + --text-secondary: #646464; + --border-color: rgba(0, 0, 0, 0.1); + color: var(--text-primary); + background-color: var(--background); + } + + button { + background-color: #f9f9f9; + } + + .message-content code { + background-color: rgba(0, 0, 0, 0.05); + } + + .message-content pre { + background-color: rgba(0, 0, 0, 0.03); + } + + .tool-calls, + .tool-results { + background-color: rgba(0, 0, 0, 0.03); + } + + .tool-calls pre, + .tool-results pre { + background-color: rgba(0, 0, 0, 0.05); + } +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/main.tsx b/hyperdrive/packages/spider/ui/src/main.tsx new file mode 100644 index 000000000..a3ad16d4d --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/main.tsx @@ -0,0 +1,12 @@ +// Entry point for the React application +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +// Create root and render the app +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/services/websocket.ts b/hyperdrive/packages/spider/ui/src/services/websocket.ts new file mode 100644 index 000000000..33c275358 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/services/websocket.ts @@ -0,0 +1,174 @@ +import { Message, ConversationMetadata } from '@caller-utils'; +import { + WsClientMessage, + WsServerMessage, + AuthMessage, + ChatMessage, + CancelMessage, + PingMessage +} from '../types/websocket'; + +export type MessageHandler = (message: WsServerMessage) => void; + +class WebSocketService { + private ws: WebSocket | null = null; + private messageHandlers: Set = new Set(); + private reconnectTimeout: NodeJS.Timeout | null = null; + private url: string = ''; + private isAuthenticated: boolean = false; + + connect(url: string): Promise { + return new Promise((resolve, reject) => { + if (this.ws?.readyState === WebSocket.OPEN) { + resolve(); + return; + } + + this.url = url; + this.ws = new WebSocket(url); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.clearReconnectTimeout(); + resolve(); + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + reject(error); + }; + + this.ws.onclose = () => { + console.log('WebSocket disconnected'); + this.isAuthenticated = false; + this.scheduleReconnect(); + }; + + this.ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data) as WsServerMessage; + this.handleMessage(message); + } catch (error) { + console.error('Failed to parse WebSocket message:', error); + } + }; + }); + } + + private handleMessage(message: WsServerMessage) { + // Notify all handlers + this.messageHandlers.forEach(handler => handler(message)); + } + + authenticate(apiKey: string): Promise { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + reject(new Error('WebSocket not connected')); + return; + } + + // Set up one-time handler for auth response + const authHandler = (message: WsServerMessage) => { + if (message.type === 'auth_success') { + this.isAuthenticated = true; + this.removeMessageHandler(authHandler); + resolve(); + } else if (message.type === 'auth_error') { + this.removeMessageHandler(authHandler); + reject(new Error(message.error || 'Authentication failed')); + } + }; + + this.addMessageHandler(authHandler); + + // Send auth message + const authMsg: AuthMessage = { + type: 'auth', + apiKey + }; + this.send(authMsg); + }); + } + + sendChatMessage(messages: Message[], llmProvider?: string, model?: string, mcpServers?: string[], metadata?: ConversationMetadata): void { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const chatMsg: ChatMessage = { + type: 'chat', + payload: { + messages, + llmProvider, + model, + mcpServers, + metadata + } + }; + this.send(chatMsg); + } + + sendCancel(): void { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const cancelMsg: CancelMessage = { + type: 'cancel' + }; + this.send(cancelMsg); + } + + send(data: WsClientMessage): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket not connected'); + } + + this.ws.send(JSON.stringify(data)); + } + + addMessageHandler(handler: MessageHandler): void { + this.messageHandlers.add(handler); + } + + removeMessageHandler(handler: MessageHandler): void { + this.messageHandlers.delete(handler); + } + + disconnect(): void { + this.clearReconnectTimeout(); + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.isAuthenticated = false; + } + + private scheduleReconnect(): void { + if (this.reconnectTimeout) return; + + this.reconnectTimeout = setTimeout(() => { + console.log('Attempting to reconnect WebSocket...'); + this.connect(this.url).catch(error => { + console.error('Reconnection failed:', error); + }); + }, 3000); + } + + private clearReconnectTimeout(): void { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } + } + + get isConnected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; + } + + get isReady(): boolean { + return this.isConnected && this.isAuthenticated; + } +} + +export const webSocketService = new WebSocketService(); \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/store/spider.ts b/hyperdrive/packages/spider/ui/src/store/spider.ts new file mode 100644 index 000000000..7c15b96a5 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/store/spider.ts @@ -0,0 +1,605 @@ +import { create } from 'zustand'; +import * as api from '../utils/api'; +import { webSocketService } from '../services/websocket'; +import { WsServerMessage } from '../types/websocket'; +import { AuthAnthropic } from '../auth/anthropic'; + +interface ApiKeyInfo { + provider: string; + createdAt: number; + lastUsed?: number; + keyPreview: string; +} + +interface SpiderApiKey { + key: string; + name: string; + permissions: string[]; + createdAt: number; +} + +interface McpServer { + id: string; + name: string; + transport: { + transportType: string; + command?: string; + args?: string[]; + url?: string; + hypergridToken?: string; + hypergridClientId?: string; + hypergridNode?: string; + }; + tools: Array<{ + name: string; + description: string; + parameters: string; + inputSchemaJson?: string; + }>; + connected: boolean; +} + +interface ConversationMetadata { + startTime: string; + client: string; + fromStt: boolean; +} + +interface Conversation { + id: string; + messages: Message[]; + metadata: ConversationMetadata; + llmProvider: string; + model?: string; + mcpServers: string[]; +} + +interface Message { + role: string; + content: string; + toolCallsJson?: string; + toolResultsJson?: string; + timestamp: number; +} + +interface SpiderConfig { + defaultLlmProvider: string; + maxTokens: number; + temperature: number; +} + +interface SpiderStore { + // State + apiKeys: ApiKeyInfo[]; + spiderKeys: SpiderApiKey[]; + mcpServers: McpServer[]; + conversations: Conversation[]; + activeConversation: Conversation | null; + config: SpiderConfig; + isLoading: boolean; + error: string | null; + isConnected: boolean; + nodeId: string; + currentRequestId: string | null; + useWebSocket: boolean; + wsConnected: boolean; + + // Actions + initialize: () => Promise; + setApiKey: (provider: string, key: string) => Promise; + removeApiKey: (provider: string) => Promise; + loadApiKeys: () => Promise; + createSpiderKey: (name: string, permissions: string[]) => Promise; + revokeSpiderKey: (key: string) => Promise; + loadSpiderKeys: () => Promise; + addMcpServer: (name: string, transport: any) => Promise; + connectMcpServer: (serverId: string) => Promise; + disconnectMcpServer: (serverId: string) => Promise; + removeMcpServer: (serverId: string) => Promise; + loadMcpServers: () => Promise; + sendMessage: (message: string, signal?: AbortSignal) => Promise; + cancelRequest: () => Promise; + clearActiveConversation: () => void; + loadConversations: (client?: string, limit?: number) => Promise; + loadConversation: (id: string) => Promise; + loadConfig: () => Promise; + updateConfig: (config: Partial) => Promise; + clearError: () => void; + toggleWebSocket: () => Promise; + connectWebSocket: () => Promise; + disconnectWebSocket: () => void; +} + +// Helper function to fetch admin key using generated binding +async function fetchAdminKey(): Promise { + return api.getAdminKey(); +} + +// Helper function to get API key for chat (checks OAuth tokens) +async function getApiKeyForChat(provider: string): Promise { + // Check if we have an OAuth token for Anthropic + if (provider === 'anthropic' || provider === 'anthropic-oauth') { + const oauthData = localStorage.getItem('claude_oauth'); + if (oauthData) { + try { + const tokens = JSON.parse(oauthData); + + // Check if token is expired + if (tokens.expires && tokens.expires > Date.now()) { + return tokens.access; + } + + // Try to refresh the token + const refreshed = await AuthAnthropic.refresh(tokens.refresh); + + // Update stored tokens + localStorage.setItem('claude_oauth', JSON.stringify({ + refresh: refreshed.refresh, + access: refreshed.access, + expires: refreshed.expires, + })); + + return refreshed.access; + } catch (error) { + console.error('Failed to refresh OAuth token:', error); + // Remove invalid tokens + localStorage.removeItem('claude_oauth'); + } + } + } + + // Fall back to admin key + return (window as any).__spiderAdminKey || null; +} + +export const useSpiderStore = create((set, get) => ({ + // Initial state + apiKeys: [], + spiderKeys: [], + mcpServers: [], + conversations: [], + activeConversation: null, + config: { + defaultLlmProvider: 'anthropic', + maxTokens: 4096, + temperature: 0.7, + }, + isLoading: false, + error: null, + isConnected: false, + nodeId: '', + currentRequestId: null, + useWebSocket: true, // Default to WebSocket for progressive updates + wsConnected: false, + + // Actions + initialize: async () => { + try { + // Fetch admin key if no spider keys are loaded + const state = get(); + if (!state.spiderKeys || state.spiderKeys.length === 0) { + try { + const adminKey = await fetchAdminKey(); + // Store the admin key in a variable accessible to API calls + (window as any).__spiderAdminKey = adminKey; + } catch (error) { + console.error('Failed to fetch admin key:', error); + } + } + set({ isLoading: true }); + + // Check if our.js is loaded + if (typeof window.our === 'undefined') { + set({ + isConnected: false, + error: 'Not connected to Hyperware node. Make sure you are running on a Hyperware node.', + isLoading: false + }); + return; + } + + const nodeId = window.our.node; + set({ isConnected: true, nodeId }); + + // Load initial data + await Promise.all([ + get().loadApiKeys(), + get().loadSpiderKeys(), + get().loadMcpServers(), + get().loadConfig(), + ]); + + // Try to connect WebSocket for progressive updates + if (get().useWebSocket) { + await get().connectWebSocket(); + } + + set({ isLoading: false }); + } catch (error: any) { + set({ + error: error.message || 'Failed to initialize', + isLoading: false, + isConnected: false + }); + } + }, + + setApiKey: async (provider: string, key: string) => { + try { + set({ isLoading: true, error: null }); + await api.setApiKey(provider, key); + await get().loadApiKeys(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to set API key', isLoading: false }); + } + }, + + removeApiKey: async (provider: string) => { + try { + set({ isLoading: true, error: null }); + await api.removeApiKey(provider); + await get().loadApiKeys(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to remove API key', isLoading: false }); + } + }, + + loadApiKeys: async () => { + try { + const keys = await api.listApiKeys(); + set({ apiKeys: keys }); + } catch (error: any) { + set({ error: error.message || 'Failed to load API keys' }); + } + }, + + createSpiderKey: async (name: string, permissions: string[]) => { + try { + set({ isLoading: true, error: null }); + await api.createSpiderKey(name, permissions); + await get().loadSpiderKeys(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to create Spider key', isLoading: false }); + } + }, + + revokeSpiderKey: async (key: string) => { + try { + set({ isLoading: true, error: null }); + await api.revokeSpiderKey(key); + await get().loadSpiderKeys(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to revoke Spider key', isLoading: false }); + } + }, + + loadSpiderKeys: async () => { + try { + // Ensure admin key is fetched first if not already present + if (!(window as any).__spiderAdminKey) { + try { + const adminKey = await fetchAdminKey(); + (window as any).__spiderAdminKey = adminKey; + } catch (error) { + console.error('Failed to fetch admin key:', error); + } + } + + const keys = await api.listSpiderKeys(); + set({ spiderKeys: keys }); + } catch (error: any) { + set({ error: error.message || 'Failed to load Spider keys' }); + } + }, + + addMcpServer: async (name: string, transport: any) => { + try { + set({ isLoading: true, error: null }); + await api.addMcpServer(name, transport); + await get().loadMcpServers(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to add MCP server', isLoading: false }); + } + }, + + connectMcpServer: async (serverId: string) => { + try { + set({ isLoading: true, error: null }); + await api.connectMcpServer(serverId); + await get().loadMcpServers(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to connect MCP server', isLoading: false }); + } + }, + + disconnectMcpServer: async (serverId: string) => { + try { + set({ isLoading: true, error: null }); + await api.disconnectMcpServer(serverId); + await get().loadMcpServers(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to disconnect MCP server', isLoading: false }); + } + }, + + removeMcpServer: async (serverId: string) => { + try { + set({ isLoading: true, error: null }); + await api.removeMcpServer(serverId); + await get().loadMcpServers(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to remove MCP server', isLoading: false }); + } + }, + + loadMcpServers: async () => { + try { + const servers = await api.listMcpServers(); + set({ mcpServers: servers }); + } catch (error: any) { + set({ error: error.message || 'Failed to load MCP servers' }); + } + }, + + sendMessage: async (message: string, signal?: AbortSignal) => { + try { + const requestId = Math.random().toString(36).substring(7); + set({ isLoading: true, error: null, currentRequestId: requestId }); + + // Get current conversation or create new one + let conversation = get().activeConversation; + if (!conversation) { + conversation = { + id: '', + messages: [], + metadata: { + startTime: new Date().toISOString(), + client: 'web-ui', + fromStt: false, + }, + llmProvider: get().config.defaultLlmProvider, + model: get().config.defaultLlmProvider === 'anthropic' ? 'claude-sonnet-4-20250514' : undefined, + mcpServers: get().mcpServers.filter(s => s.connected).map(s => s.id), + }; + } + + // Add user message + const userMessage: Message = { + role: 'user', + content: message, + toolCallsJson: null, + toolResultsJson: null, + timestamp: Date.now(), + }; + + // Update local state immediately for better UX + conversation.messages.push(userMessage); + set({ activeConversation: { ...conversation } }); + + // Check if we should use WebSocket + if (get().useWebSocket && webSocketService.isReady) { + // Send via WebSocket for progressive updates + webSocketService.sendChatMessage( + conversation.messages, + conversation.llmProvider, + conversation.model, + conversation.mcpServers, + conversation.metadata + ); + // WebSocket responses will be handled by the message handler + return; + } + + // Fallback to HTTP + // Get appropriate API key (OAuth or admin) + const apiKey = await getApiKeyForChat(conversation.llmProvider); + if (!apiKey) { + throw new Error('No valid API key available. Please add an API key or login with Claude.'); + } + + // Send to backend with abort signal support + const response = await api.chat( + apiKey, + conversation.messages, + conversation.llmProvider, + conversation.model, + conversation.mcpServers, + conversation.metadata, + signal + ); + + // Only update if this request hasn't been cancelled + if (get().currentRequestId === requestId) { + // Update conversation with response + conversation.id = response.conversationId; + + // Add all messages from the response (includes tool calls and results) + if (response.allMessages && response.allMessages.length > 0) { + conversation.messages.push(...response.allMessages); + } else { + // Fallback to just the final response if allMessages is not available + conversation.messages.push(response.response); + } + + // Update conversations list + const conversations = get().conversations; + const existingIndex = conversations.findIndex(c => c.id === conversation.id); + if (existingIndex >= 0) { + conversations[existingIndex] = conversation; + } else { + conversations.unshift(conversation); + } + + set({ + activeConversation: { ...conversation }, + conversations: [...conversations], + isLoading: false, + currentRequestId: null + }); + } + } catch (error: any) { + if (error.name === 'AbortError') { + set({ isLoading: false, currentRequestId: null }); + } else { + set({ + error: error.message || 'Failed to send message', + isLoading: false, + currentRequestId: null + }); + } + } + }, + + cancelRequest: async () => { + set({ currentRequestId: null, isLoading: false }); + // TODO: Send cancel request to backend if needed + }, + + clearActiveConversation: () => { + set({ activeConversation: null }); + }, + + loadConversations: async (client?: string, limit?: number) => { + try { + const conversations = await api.listConversations(client, limit); + set({ conversations }); + } catch (error: any) { + set({ error: error.message || 'Failed to load conversations' }); + } + }, + + loadConversation: async (id: string) => { + try { + const conversation = await api.getConversation(id); + set({ activeConversation: conversation }); + } catch (error: any) { + set({ error: error.message || 'Failed to load conversation' }); + } + }, + + loadConfig: async () => { + try { + const config = await api.getConfig(); + set({ config }); + } catch (error: any) { + set({ error: error.message || 'Failed to load config' }); + } + }, + + updateConfig: async (config: Partial) => { + try { + set({ isLoading: true, error: null }); + await api.updateConfig(config); + await get().loadConfig(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to update config', isLoading: false }); + } + }, + + clearError: () => set({ error: null }), + + toggleWebSocket: async () => { + const newState = !get().useWebSocket; + set({ useWebSocket: newState }); + + if (newState) { + await get().connectWebSocket(); + } else { + get().disconnectWebSocket(); + } + }, + + connectWebSocket: async () => { + try { + // Determine WebSocket URL based on current location and BASE_URL + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = window.location.host; + const baseUrl = import.meta.env.BASE_URL || '/'; + const wsPath = baseUrl.endsWith('/') ? `${baseUrl}ws` : `${baseUrl}/ws`; + const wsUrl = `${protocol}//${host}${wsPath}`; + + console.log('Connecting to WebSocket at:', wsUrl); + await webSocketService.connect(wsUrl); + + // Set up message handler for progressive updates + webSocketService.addMessageHandler((message: WsServerMessage) => { + const state = get(); + + switch (message.type) { + case 'message': + // Progressive message update from tool loop + if (state.activeConversation && message.message) { + const updatedConversation = { ...state.activeConversation }; + updatedConversation.messages.push(message.message); + set({ activeConversation: updatedConversation }); + } + break; + + case 'chat_complete': + // Final response received + if (state.activeConversation && message.payload) { + const updatedConversation = { ...state.activeConversation }; + updatedConversation.id = message.payload.conversationId; + + // Update conversations list + const conversations = [...state.conversations]; + const existingIndex = conversations.findIndex(c => c.id === updatedConversation.id); + if (existingIndex >= 0) { + conversations[existingIndex] = updatedConversation; + } else { + conversations.unshift(updatedConversation); + } + + set({ + activeConversation: updatedConversation, + conversations, + isLoading: false, + currentRequestId: null + }); + } + break; + + case 'error': + set({ + error: message.error || 'WebSocket error occurred', + isLoading: false, + currentRequestId: null + }); + break; + } + }); + + // Authenticate with appropriate API key (OAuth or admin) + const provider = get().config.defaultLlmProvider; + const authKey = await getApiKeyForChat(provider); + if (!authKey) { + console.error('No API key available for WebSocket auth'); + throw new Error('No valid API key available. Please add an API key or login with Claude.'); + } + await webSocketService.authenticate(authKey); + + set({ wsConnected: true }); + } catch (error: any) { + console.error('Failed to connect WebSocket:', error); + set({ + wsConnected: false, + useWebSocket: false, + error: 'Failed to connect WebSocket. Falling back to HTTP.' + }); + } + }, + + disconnectWebSocket: () => { + webSocketService.disconnect(); + set({ wsConnected: false }); + }, +})); \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/types/caller-utils.d.ts b/hyperdrive/packages/spider/ui/src/types/caller-utils.d.ts new file mode 100644 index 000000000..9d01ecbe5 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/types/caller-utils.d.ts @@ -0,0 +1,16 @@ +declare module '../../target/ui/caller-utils' { + export function setApiKey(requestBody: string): Promise; + export function listApiKeys(requestBody: string): Promise; + export function removeApiKey(provider: string): Promise; + export function createSpiderKey(requestBody: string): Promise; + export function listSpiderKeys(requestBody: string): Promise; + export function revokeSpiderKey(keyId: string): Promise; + export function addMcpServer(requestBody: string): Promise; + export function listMcpServers(requestBody: string): Promise; + export function connectMcpServer(serverId: string): Promise; + export function listConversations(requestBody: string): Promise; + export function getConversation(conversationId: string): Promise; + export function getConfig(requestBody: string): Promise; + export function updateConfig(requestBody: string): Promise; + export function chat(requestBody: string): Promise; +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/types/global.ts b/hyperdrive/packages/spider/ui/src/types/global.ts new file mode 100644 index 000000000..f4b35ac55 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/types/global.ts @@ -0,0 +1,32 @@ +// Global type definitions for Hyperware environment + +// The window.our object is provided by the /our.js script +// It contains the node and process identity +declare global { + interface Window { + our?: { + node: string; // e.g., "alice.os" + process: string; // e.g., "skeleton-app:skeleton-app:skeleton.os" + }; + } +} + +// Base URL for API calls +// In production, this is empty (same origin) +// In development, you might proxy to your local node +export const BASE_URL = ''; + +// Helper to check if we're in a Hyperware environment +export const isHyperwareEnvironment = (): boolean => { + return typeof window !== 'undefined' && window.our !== undefined; +}; + +// Get the current node identity +export const getNodeId = (): string | null => { + return window.our?.node || null; +}; + +// Get the current process identity +export const getProcessId = (): string | null => { + return window.our?.process || null; +}; \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/types/websocket.ts b/hyperdrive/packages/spider/ui/src/types/websocket.ts new file mode 100644 index 000000000..f7891ebe5 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/types/websocket.ts @@ -0,0 +1,86 @@ +// WebSocket message types for Spider chat + +import { Message, ConversationMetadata, ChatResponse } from '@caller-utils'; + +// Client -> Server messages +export type WsClientMessage = + | AuthMessage + | ChatMessage + | CancelMessage + | PingMessage; + +export interface AuthMessage { + type: 'auth'; + apiKey: string; +} + +export interface ChatMessage { + type: 'chat'; + payload: { + messages: Message[]; + llmProvider?: string; + mcpServers?: string[]; + metadata?: ConversationMetadata; + }; +} + +export interface CancelMessage { + type: 'cancel'; +} + +export interface PingMessage { + type: 'ping'; +} + +// Server -> Client messages +export type WsServerMessage = + | AuthSuccessMessage + | AuthErrorMessage + | StatusMessage + | StreamMessage + | MessageUpdate + | ChatCompleteMessage + | ErrorMessage + | PongMessage; + +export interface AuthSuccessMessage { + type: 'auth_success'; + message: string; +} + +export interface AuthErrorMessage { + type: 'auth_error'; + error: string; +} + +export interface StatusMessage { + type: 'status'; + status: string; + message?: string; +} + +export interface StreamMessage { + type: 'stream'; + iteration: number; + message: string; + tool_calls?: string; +} + +export interface MessageUpdate { + type: 'message'; + message: Message; +} + +export interface ChatCompleteMessage { + type: 'chat_complete'; + payload: ChatResponse; +} + +export interface ErrorMessage { + type: 'error'; + error: string; +} + +export interface PongMessage { + type: 'pong'; +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/utils/api.ts b/hyperdrive/packages/spider/ui/src/utils/api.ts new file mode 100644 index 000000000..834aa2fdc --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/utils/api.ts @@ -0,0 +1,175 @@ +// Import the generated functions and types directly +import { + set_api_key as _setApiKey, + list_api_keys as _listApiKeys, + remove_api_key as _removeApiKey, + create_spider_key as _createSpiderKey, + list_spider_keys as _listSpiderKeys, + revoke_spider_key as _revokeSpiderKey, + add_mcp_server as _addMcpServer, + list_mcp_servers as _listMcpServers, + connect_mcp_server as _connectMcpServer, + disconnect_mcp_server as _disconnectMcpServer, + remove_mcp_server as _removeMcpServer, + list_conversations as _listConversations, + get_conversation as _getConversation, + get_config as _getConfig, + update_config as _updateConfig, + chat as _chat, + get_admin_key as _getAdminKey, + type ApiKeyInfo, + type SpiderApiKey, + type McpServer, + type Conversation, + type ConfigResponse, + type ChatResponse, + type Message, + type ConversationMetadata, + type TransportConfig, +} from '@caller-utils'; + +export async function getAdminKey(): Promise { + return _getAdminKey(); +} + +export async function setApiKey(provider: string, key: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _setApiKey({ provider, key, authKey }); +} + +export async function listApiKeys(): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _listApiKeys({ authKey }); +} + +export async function removeApiKey(provider: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _removeApiKey({ provider, authKey }); +} + +export async function createSpiderKey(name: string, permissions: string[]): Promise { + const adminKey = (window as any).__spiderAdminKey; + if (!adminKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _createSpiderKey({ name, permissions, adminKey }); +} + +export async function listSpiderKeys(): Promise { + const adminKey = (window as any).__spiderAdminKey; + if (!adminKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _listSpiderKeys({ adminKey }); +} + +export async function revokeSpiderKey(key: string) { + const adminKey = (window as any).__spiderAdminKey; + if (!adminKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _revokeSpiderKey({ keyId: key, adminKey }); +} + +export async function addMcpServer(name: string, transport: TransportConfig): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _addMcpServer({ name, transport, authKey }); +} + +export async function listMcpServers(): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _listMcpServers({ authKey }); +} + +export async function connectMcpServer(serverId: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _connectMcpServer({ serverId, authKey }); +} + +export async function disconnectMcpServer(serverId: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _disconnectMcpServer({ serverId, authKey }); +} + +export async function removeMcpServer(serverId: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _removeMcpServer({ serverId, authKey }); +} + +export async function listConversations(client?: string, limit?: number, offset?: number): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _listConversations({ + client: client || null, + limit: limit || null, + offset: offset || null, + authKey + }); +} + +export async function getConversation(conversationId: string): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _getConversation({ conversationId, authKey }); +} + +export async function getConfig(): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _getConfig({ authKey }); +} + +export async function updateConfig(config: Partial): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _updateConfig({ + defaultLlmProvider: config.defaultLlmProvider || null, + maxTokens: config.maxTokens || null, + temperature: config.temperature || null, + authKey + }); +} + +export async function chat(apiKey: string, messages: Message[], llmProvider?: string, model?: string, mcpServers?: string[], metadata?: ConversationMetadata, signal?: AbortSignal): Promise { + // TODO: Pass signal to the underlying API call when supported + return _chat({ + apiKey, + messages, + llmProvider: llmProvider || null, + model: model || null, + mcpServers: mcpServers || null, + metadata: metadata || null + }); +} diff --git a/hyperdrive/packages/spider/ui/src/vite-env.d.ts b/hyperdrive/packages/spider/ui/src/vite-env.d.ts new file mode 100644 index 000000000..151aa6856 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/tsconfig.json b/hyperdrive/packages/spider/ui/tsconfig.json new file mode 100644 index 000000000..d07dc20e1 --- /dev/null +++ b/hyperdrive/packages/spider/ui/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + + /* Path mappings */ + "baseUrl": ".", + "paths": { + "@caller-utils": ["../target/ui/caller-utils"] + } + }, + "include": ["src"], + "exclude": ["../target/ui/caller-utils.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/tsconfig.node.json b/hyperdrive/packages/spider/ui/tsconfig.node.json new file mode 100644 index 000000000..4eb43d054 --- /dev/null +++ b/hyperdrive/packages/spider/ui/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/vite.config.ts b/hyperdrive/packages/spider/ui/vite.config.ts new file mode 100644 index 000000000..1c9b21c6b --- /dev/null +++ b/hyperdrive/packages/spider/ui/vite.config.ts @@ -0,0 +1,74 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +/* +If you are developing a UI outside of a Hyperware project, +comment out the following 2 lines: +*/ +import manifest from '../pkg/manifest.json' +import metadata from '../metadata.json' + +/* +IMPORTANT: +This must match the process name from pkg/manifest.json + pkg/metadata.json +The format is "/" + "process_name:package_name:publisher_node" +*/ +const BASE_URL = `/${manifest[0].process_name}:${metadata.properties.package_name}:${metadata.properties.publisher}`; + +// This is the proxy URL, it must match the node you are developing against +const PROXY_URL = (process.env.VITE_NODE_URL || 'http://127.0.0.1:8080').replace('localhost', '127.0.0.1'); + +console.log('process.env.VITE_NODE_URL', process.env.VITE_NODE_URL, PROXY_URL); + +export default defineConfig({ + plugins: [react()], + base: BASE_URL, + resolve: { + alias: { + '@caller-utils': path.resolve(__dirname, '../target/ui/caller-utils.ts') + } + }, + build: { + rollupOptions: { + external: ['/our.js'] + } + }, + server: { + open: true, + proxy: { + '/our': { + target: PROXY_URL, + changeOrigin: true, + }, + [`${BASE_URL}/our.js`]: { + target: PROXY_URL, + changeOrigin: true, + rewrite: (path) => path.replace(BASE_URL, ''), + }, + // This route will match all other HTTP requests to the backend + [`^${BASE_URL}/(?!(@vite/client|src/.*|node_modules/.*|@react-refresh|$))`]: { + target: PROXY_URL, + changeOrigin: true, + ws: true, // Enable WebSocket proxy + }, + // '/example': { + // target: PROXY_URL, + // changeOrigin: true, + // rewrite: (path) => path.replace(BASE_URL, ''), + // // This is only for debugging purposes + // configure: (proxy, _options) => { + // proxy.on('error', (err, _req, _res) => { + // console.log('proxy error', err); + // }); + // proxy.on('proxyReq', (proxyReq, req, _res) => { + // console.log('Sending Request to the Target:', req.method, req.url); + // }); + // proxy.on('proxyRes', (proxyRes, req, _res) => { + // console.log('Received Response from the Target:', proxyRes.statusCode, req.url); + // }); + // }, + // }, + } + } +}); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b9c53141d..a3e17ac41 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lib" authors = ["Sybil Technologies AG"] -version = "1.6.1" +version = "1.7.0" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" diff --git a/scripts/build-packages/Cargo.toml b/scripts/build-packages/Cargo.toml index cc7bdcb1c..6ea81bb24 100644 --- a/scripts/build-packages/Cargo.toml +++ b/scripts/build-packages/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" anyhow = "1.0.71" clap = "4" fs-err = "2.11" -kit = { git = "https://github.com/hyperware-ai/kit", rev = "79fe678" } +kit = { git = "https://github.com/hyperware-ai/kit", rev = "aac33b6" } serde = "1" serde_json = "1" tokio = "1.28" diff --git a/scripts/build-packages/src/main.rs b/scripts/build-packages/src/main.rs index 980f6166a..b7a439139 100644 --- a/scripts/build-packages/src/main.rs +++ b/scripts/build-packages/src/main.rs @@ -12,11 +12,13 @@ use zip::write::FileOptions; struct PackageBuildParameters { local_dependencies: Option>, is_hyperapp: Option, + features: Option>, } struct PackageBuildParametersPath { local_dependencies: Option>, is_hyperapp: Option, + features: Option>, } fn zip_directory(dir_path: &Path) -> anyhow::Result> { @@ -148,6 +150,7 @@ fn main() -> anyhow::Result<()> { .local_dependencies .map(|bp| bp.iter().map(|f| packages_dir.join(f)).collect()), is_hyperapp: val.is_hyperapp, + features: val.features, }, ) }) @@ -164,7 +167,7 @@ fn main() -> anyhow::Result<()> { // don't run on, e.g., `.DS_Store` return None; } - let (local_dependency_array, is_hyperapp) = + let (local_dependency_array, is_hyperapp, package_specific_features) = if let Some(filename) = entry_path.file_name() { if let Some(maybe_params) = build_parameters.remove(&filename.to_string_lossy().to_string()) @@ -172,18 +175,36 @@ fn main() -> anyhow::Result<()> { ( maybe_params.local_dependencies.unwrap_or_default(), maybe_params.is_hyperapp.unwrap_or_default(), + maybe_params.features.unwrap_or_default(), ) } else { - (vec![], false) + (vec![], false, vec![]) } } else { - (vec![], false) + (vec![], false, vec![]) }; + let package_specific_features = if package_specific_features.is_empty() { + features.clone() + } else if package_specific_features.contains(&"caller-utils".to_string()) { + // build without caller-utils flag, which will fail but will + // also create caller-utils crate (required for succeeding build) + let _ = build_and_zip_package( + entry_path.clone(), + child_pkg_path.to_str().unwrap(), + skip_frontend, + &features, + local_dependency_array.clone(), + is_hyperapp, + ); + format!("{features},{}", package_specific_features.join(",")) + } else { + format!("{features},{}", package_specific_features.join(",")) + }; Some(build_and_zip_package( entry_path.clone(), child_pkg_path.to_str().unwrap(), skip_frontend, - &features, + &package_specific_features, local_dependency_array, is_hyperapp, )) From c2ebb6ba915f539ab7c5f7d597514f8281ad8f88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 23:02:38 +0000 Subject: [PATCH 2/3] Format Rust code using rustfmt --- .../packages/file-explorer/explorer/src/lib.rs | 6 ++---- hyperdrive/packages/spider/spider/src/lib.rs | 16 ++++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/hyperdrive/packages/file-explorer/explorer/src/lib.rs b/hyperdrive/packages/file-explorer/explorer/src/lib.rs index 5aabd4943..ec0b85a60 100644 --- a/hyperdrive/packages/file-explorer/explorer/src/lib.rs +++ b/hyperdrive/packages/file-explorer/explorer/src/lib.rs @@ -1,12 +1,10 @@ use hyperprocess_macro::hyperprocess; -use hyperware_process_lib::logging::{ - debug, error, info, init_logging, Level, -}; +use hyperware_process_lib::hyperapp::{add_response_header, get_path, send, SaveOptions}; +use hyperware_process_lib::logging::{debug, error, info, init_logging, Level}; use hyperware_process_lib::our; use hyperware_process_lib::vfs::{ self, create_drive, vfs_request, FileType, VfsAction, VfsResponse, }; -use hyperware_process_lib::hyperapp::{add_response_header, get_path, send, SaveOptions}; use std::collections::HashMap; const ICON: &str = include_str!("./icon"); diff --git a/hyperdrive/packages/spider/spider/src/lib.rs b/hyperdrive/packages/spider/spider/src/lib.rs index 485a55e2b..8b6c5ceb8 100644 --- a/hyperdrive/packages/spider/spider/src/lib.rs +++ b/hyperdrive/packages/spider/spider/src/lib.rs @@ -6,8 +6,6 @@ use chrono::Utc; use serde_json::Value; use uuid::Uuid; -#[cfg(not(feature = "simulation-mode"))] -use spider_caller_utils::anthropic_api_key_manager::request_api_key_remote_rpc; use hyperprocess_macro::*; use hyperware_process_lib::{ homepage::add_to_homepage, @@ -18,6 +16,8 @@ use hyperware_process_lib::{ hyperapp::source, our, println, Address, LazyLoadBlob, ProcessId, }; +#[cfg(not(feature = "simulation-mode"))] +use spider_caller_utils::anthropic_api_key_manager::request_api_key_remote_rpc; mod provider; use provider::create_llm_provider; @@ -44,7 +44,7 @@ use utils::{ }; mod tool_providers; -use tool_providers::{ToolProvider, hypergrid::HypergridToolProvider}; +use tool_providers::{hypergrid::HypergridToolProvider, ToolProvider}; const ICON: &str = include_str!("./icon"); @@ -108,7 +108,8 @@ impl SpiderState { let hypergrid_tools = hypergrid_provider.get_tools(self); // Register the provider for later use - self.tool_provider_registry.register(Box::new(hypergrid_provider)); + self.tool_provider_registry + .register(Box::new(hypergrid_provider)); let hypergrid_server = McpServer { id: "hypergrid_default".to_string(), @@ -117,7 +118,9 @@ impl SpiderState { transport_type: "hypergrid".to_string(), command: None, args: None, - url: Some("http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp".to_string()), + url: Some( + "http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp".to_string(), + ), hypergrid_token: None, hypergrid_client_id: None, hypergrid_node: None, @@ -957,7 +960,8 @@ impl SpiderState { // Register the provider if not already registered if !self.tool_provider_registry.has_provider(&request.server_id) { - self.tool_provider_registry.register(Box::new(hypergrid_provider)); + self.tool_provider_registry + .register(Box::new(hypergrid_provider)); } // Update the server with hypergrid tools and mark as connected From e5d96363a73e4a0eccf76db2bef900a4f113e0c5 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 5 Sep 2025 16:29:25 -0700 Subject: [PATCH 3/3] spider: fix api key dispenser publisher --- hyperdrive/packages/spider/spider/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyperdrive/packages/spider/spider/src/lib.rs b/hyperdrive/packages/spider/spider/src/lib.rs index 8b6c5ceb8..8ed1d71f0 100644 --- a/hyperdrive/packages/spider/spider/src/lib.rs +++ b/hyperdrive/packages/spider/spider/src/lib.rs @@ -56,7 +56,7 @@ const API_KEY_DISPENSER_NODE: &str = "fake.os"; const API_KEY_DISPENSER_PROCESS_ID: (&str, &str, &str) = ( "anthropic-api-key-manager", "anthropic-api-key-manager", - "sys", + "ware.hypr", ); const HYPERGRID: &str = "operator:hypergrid:ware.hypr";