mirror of
https://github.com/runebookai/tome.git
synced 2025-07-21 00:27:30 +03:00
Smithery Catalog
Lists Smithery `local` MCP servers.
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -90,6 +90,9 @@ importers:
|
||||
globals:
|
||||
specifier: ^16.0.0
|
||||
version: 16.0.0
|
||||
json-schema:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
postcss:
|
||||
specifier: ^8.5.3
|
||||
version: 8.5.3
|
||||
@@ -1424,6 +1427,9 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-schema@0.4.0:
|
||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -3382,6 +3388,8 @@ snapshots:
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema@0.4.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json5@1.0.2:
|
||||
|
||||
324
src-tauri/Cargo.lock
generated
324
src-tauri/Cargo.lock
generated
@@ -15,6 +15,7 @@ dependencies = [
|
||||
"log-panics",
|
||||
"markdown",
|
||||
"pin-project-lite",
|
||||
"reqwest",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -666,9 +667,9 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
@@ -681,7 +682,7 @@ checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"libc",
|
||||
"objc",
|
||||
@@ -748,6 +749,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.0"
|
||||
@@ -771,9 +782,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -784,7 +795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1126,6 +1137,15 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
@@ -1288,6 +1308,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1295,7 +1324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1309,6 +1338,12 @@ dependencies = [
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1761,6 +1796,25 @@ dependencies = [
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap 2.7.1",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1910,6 +1964,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
@@ -1938,6 +1993,22 @@ dependencies = [
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.10"
|
||||
@@ -2546,6 +2617,23 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2941,6 +3029,50 @@ dependencies = [
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -3610,24 +3742,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.12"
|
||||
version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
@@ -3639,7 +3775,9 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
@@ -3650,7 +3788,7 @@ dependencies = [
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry 0.2.0",
|
||||
"windows-registry 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3886,6 +4024,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
@@ -3925,6 +4072,29 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.22.0"
|
||||
@@ -4210,7 +4380,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -4585,6 +4755,27 @@ dependencies = [
|
||||
"windows 0.57.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -4605,7 +4796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
@@ -5172,6 +5363,16 @@ dependencies = [
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.2"
|
||||
@@ -5918,7 +6119,7 @@ dependencies = [
|
||||
"windows-interface 0.59.0",
|
||||
"windows-link",
|
||||
"windows-result 0.3.1",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5993,13 +6194,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-result 0.3.1",
|
||||
"windows-strings",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6010,7 +6211,7 @@ checksum = "6c44a98275e31bfd112bb06ba96c8ab13c03383a3753fdddd715406a1824c7e0"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result 0.3.1",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6022,15 +6223,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.1"
|
||||
@@ -6040,16 +6232,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.3.1"
|
||||
@@ -6134,13 +6316,29 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
"windows_i686_gnullvm 0.53.0",
|
||||
"windows_i686_msvc 0.53.0",
|
||||
"windows_x86_64_gnu 0.53.0",
|
||||
"windows_x86_64_gnullvm 0.53.0",
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.3"
|
||||
@@ -6168,6 +6366,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -6186,6 +6390,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -6204,12 +6414,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -6228,6 +6450,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -6246,6 +6474,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -6264,6 +6498,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -6282,6 +6522,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
|
||||
@@ -37,6 +37,7 @@ tauri-plugin-log = "2"
|
||||
tauri-plugin-fs = "2.2.1"
|
||||
pin-project-lite = "0.2.16"
|
||||
tauri-plugin-opener = "2"
|
||||
reqwest = "0.12.15"
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
cocoa = "0.26.0"
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rmcp::model::Tool;
|
||||
use tauri::AppHandle;
|
||||
|
||||
@@ -7,21 +9,32 @@ use crate::State;
|
||||
macro_rules! ok_or_err {
|
||||
($expr:expr) => {
|
||||
match $expr {
|
||||
Ok(e) => Ok(e),
|
||||
Err(_) => Err(()),
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_metadata(command: String, app: AppHandle) -> Result<String, ()> {
|
||||
ok_or_err!(mcp::peer_info(command, app).await)
|
||||
pub async fn get_metadata(
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
app: AppHandle,
|
||||
) -> Result<String, String> {
|
||||
ok_or_err!(mcp::peer_info(command, args, env, app).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_mcp_server(session_id: i32, command: String, app: AppHandle) -> Result<(), ()> {
|
||||
pub async fn start_mcp_server(
|
||||
session_id: i32,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
app: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
println!("-> start_mcp_server({}, {})", session_id, command);
|
||||
ok_or_err!(mcp::start(session_id, command, app).await)
|
||||
ok_or_err!(mcp::start(session_id, command, args, env, app).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -29,7 +42,7 @@ pub async fn stop_mcp_server(
|
||||
session_id: i32,
|
||||
name: String,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<(), ()> {
|
||||
) -> Result<(), String> {
|
||||
println!("-> stop_mcp_server({}, {})", session_id, name);
|
||||
ok_or_err!(mcp::stop(session_id, name, state).await)
|
||||
}
|
||||
@@ -38,7 +51,7 @@ pub async fn stop_mcp_server(
|
||||
pub async fn get_mcp_tools(
|
||||
session_id: i32,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<Vec<Tool>, ()> {
|
||||
) -> Result<Vec<Tool>, String> {
|
||||
ok_or_err!(mcp::get_tools(session_id, state).await)
|
||||
}
|
||||
|
||||
@@ -48,13 +61,13 @@ pub async fn call_mcp_tool(
|
||||
name: String,
|
||||
arguments: serde_json::Map<String, serde_json::Value>,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<String, ()> {
|
||||
) -> Result<String, String> {
|
||||
Ok(mcp::call_tool(session_id, name, arguments, state)
|
||||
.await
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_session(session_id: i32, state: tauri::State<'_, State>) -> Result<(), ()> {
|
||||
pub async fn stop_session(session_id: i32, state: tauri::State<'_, State>) -> Result<(), String> {
|
||||
ok_or_err!(mcp::stop_session(session_id, state).await)
|
||||
}
|
||||
|
||||
102
src-tauri/src/http.rs
Normal file
102
src-tauri/src/http.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::http::{HeaderMap, HeaderName, HeaderValue};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
enum HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
HEAD,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProxyOptions {
|
||||
method: HttpMethod,
|
||||
body: Option<String>,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct HTTPResponse {
|
||||
status: u16,
|
||||
status_text: String,
|
||||
headers: HashMap<String, String>,
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch(url: String, options: ProxyOptions) -> Result<HTTPResponse, String> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let request = match options.method {
|
||||
HttpMethod::GET => client.get(&url),
|
||||
HttpMethod::POST => client.post(&url),
|
||||
HttpMethod::PUT => client.put(&url),
|
||||
HttpMethod::DELETE => client.delete(&url),
|
||||
HttpMethod::HEAD => client.head(&url),
|
||||
};
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Origin", "tauri://runebook.ai".parse().unwrap());
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
|
||||
if let Some(h) = options.headers {
|
||||
for (k, v) in h.iter() {
|
||||
headers.insert(
|
||||
HeaderName::from_bytes(k.as_bytes()).unwrap(),
|
||||
HeaderValue::from_bytes(v.as_bytes()).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let request = request.headers(headers);
|
||||
|
||||
let request = if let Some(json) = options.body {
|
||||
request.body(json)
|
||||
} else {
|
||||
request
|
||||
};
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
let status = response.status().into();
|
||||
let status_text = response
|
||||
.status()
|
||||
.canonical_reason()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let headers = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
v.to_str()
|
||||
.unwrap_or("Failed to build response: invalid UTF-8 in headers")
|
||||
.to_string(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let body = match options.method {
|
||||
HttpMethod::HEAD => String::new(),
|
||||
_ => response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to build response: failed to read body: {}", e))?,
|
||||
};
|
||||
|
||||
Ok(HTTPResponse {
|
||||
status,
|
||||
status_text,
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
mod commands;
|
||||
mod deeplink;
|
||||
mod http;
|
||||
mod mcp;
|
||||
mod migrations;
|
||||
mod process;
|
||||
@@ -73,6 +74,7 @@ fn main() {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
crate::http::fetch,
|
||||
// MCP
|
||||
commands::get_metadata,
|
||||
commands::get_mcp_tools,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub(crate) mod process;
|
||||
pub(crate) mod server;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::state::State;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -41,10 +43,16 @@ pub async fn bootstrap(app: AppHandle) -> Result<()> {
|
||||
// To deal with this, we collect child pids before we launch the server and child pids after it's
|
||||
// launched. We track those, then explicitly kill them all when in `stop`.
|
||||
//
|
||||
pub async fn start(session_id: i32, command: String, app: AppHandle) -> Result<()> {
|
||||
pub async fn start(
|
||||
session_id: i32,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
app: AppHandle,
|
||||
) -> Result<()> {
|
||||
let handle = app.clone();
|
||||
let state = handle.state::<State>();
|
||||
let server = McpServer::start(command, app).await?;
|
||||
let server = McpServer::start(command, args, env, app).await?;
|
||||
|
||||
let mut sessions = state.sessions.lock().await;
|
||||
let mut session = sessions.remove(&session_id).unwrap_or_default();
|
||||
@@ -130,8 +138,13 @@ pub async fn call_tool(
|
||||
server.call_tool(tool_call).await
|
||||
}
|
||||
|
||||
pub async fn peer_info(command: String, app: AppHandle) -> Result<String> {
|
||||
let server = McpServer::start(command, app).await?;
|
||||
pub async fn peer_info(
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
app: AppHandle,
|
||||
) -> Result<String> {
|
||||
let server = McpServer::start(command, args, env, app).await?;
|
||||
let peer_info = server.peer_info();
|
||||
server.kill()?;
|
||||
Ok(serde_json::to_string(&peer_info)?)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use rmcp::service::ServiceRole;
|
||||
use rmcp::transport::IntoTransport;
|
||||
@@ -15,22 +17,23 @@ pub(crate) struct McpProcess {
|
||||
}
|
||||
|
||||
impl McpProcess {
|
||||
pub fn start(command: String, app: AppHandle) -> Result<Self> {
|
||||
let args: Vec<&str> = command.split(" ").collect();
|
||||
|
||||
let main = *args.first().unwrap();
|
||||
let args = args.clone().drain(1..).collect::<Vec<&str>>();
|
||||
|
||||
let main = match main {
|
||||
pub fn start(
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
app: AppHandle,
|
||||
) -> Result<Self> {
|
||||
let command = match &*command {
|
||||
"uvx" => app.path().resolve("uvx", BaseDirectory::Resource)?,
|
||||
"npx" => app.path().resolve("npx", BaseDirectory::Resource)?,
|
||||
s => return Err(anyhow!("{} servers not supported", s)),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(main);
|
||||
let mut cmd = Command::new(command);
|
||||
let cmd = cmd.args(args);
|
||||
|
||||
cmd.kill_on_drop(true)
|
||||
.envs(env)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped());
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::process::Process;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -19,8 +21,13 @@ pub struct McpServer {
|
||||
}
|
||||
|
||||
impl McpServer {
|
||||
pub async fn start(command: String, app: AppHandle) -> Result<Self> {
|
||||
let proc = McpProcess::start(command, app)?;
|
||||
pub async fn start(
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
app: AppHandle,
|
||||
) -> Result<Self> {
|
||||
let proc = McpProcess::start(command, args, env, app)?;
|
||||
let pid = proc.pid();
|
||||
let service = ().serve(proc).await?;
|
||||
Ok(Self { service, pid })
|
||||
|
||||
@@ -122,5 +122,23 @@ ALTER TABLE messages ADD COLUMN response_id INTEGER REFERENCES messages(id);
|
||||
"#,
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 8,
|
||||
description: "expand_mcp_servers",
|
||||
sql: r#"
|
||||
ALTER TABLE mcp_servers ADD COLUMN args JSON NOT NULL DEFAULT "[]";
|
||||
ALTER TABLE mcp_servers ADD COLUMN env JSON NOT NULL DEFAULT "{}";
|
||||
"#,
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 9,
|
||||
description: "add_explicit_name_to_mcp_servers",
|
||||
sql: r#"
|
||||
ALTER TABLE mcp_servers ADD COLUMN name TEXT NOT NULL DEFAULT "Unknown";
|
||||
UPDATE mcp_servers SET name = json_extract(metadata, '$.serverInfo.name');
|
||||
"#,
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
15
src/app.css
15
src/app.css
@@ -9,7 +9,9 @@
|
||||
--color-green: #c3e88d;
|
||||
--color-yellow: #ffc777;
|
||||
|
||||
--border-color-light: #171717;
|
||||
--border-color-light: #191919;
|
||||
--border-color-medium: #0c0c0c;
|
||||
--border-color-dark: #0b0b0b;
|
||||
|
||||
--background-color-light: #191919;
|
||||
--background-color-medium: #0c0c0c;
|
||||
@@ -38,17 +40,6 @@
|
||||
src: url("/fonts/PlusJakartaSans-Italic-VariableFont_wght.ttf");
|
||||
}
|
||||
|
||||
* {
|
||||
/* Hide scrollbar */
|
||||
-ms-overflow-style: none;
|
||||
scrolbar-width: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar */
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: #0b0b0b;
|
||||
|
||||
9
src/app.d.ts
vendored
9
src/app.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
@@ -10,16 +12,19 @@ declare global {
|
||||
}
|
||||
|
||||
// Utility type for plain, unknown-value, JS Objects
|
||||
type Obj = { [key: string]: any }; // eslint-disable-line
|
||||
type Obj = { [key: string]: any };
|
||||
|
||||
interface ObjectConstructor {
|
||||
compact<T>(o: Obj): T;
|
||||
without<T>(o: Obj, keys: string[]): T;
|
||||
remove<T>(o: Obj, key: string): T | undefined;
|
||||
map<T>(o: Obj, fn: (key: string, value: any) => any): T;
|
||||
}
|
||||
|
||||
interface Array<T> {
|
||||
sortBy(key: string): Array<T>;
|
||||
sortBy<T extends Obj>(key: string): Array<T>;
|
||||
findBy<T extends Obj>(key: string, value: any): T | undefined;
|
||||
compact(): Array<T>;
|
||||
}
|
||||
|
||||
interface CheckboxEvent extends Event {
|
||||
|
||||
13
src/components/Button.svelte
Normal file
13
src/components/Button.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const { children, class: cls = '', ...rest }: HTMLButtonAttributes = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={twMerge('rounded-md border p-2 px-6 text-sm hover:cursor-pointer', cls?.toString())}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import Scrollable from './Scrollable.svelte';
|
||||
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Message from '$components/Message.svelte';
|
||||
import { dispatch } from '$lib/dispatch';
|
||||
@@ -18,6 +20,8 @@
|
||||
|
||||
// DOM elements used via `bind:this`
|
||||
let input: HTMLTextAreaElement;
|
||||
|
||||
// svelte-ignore non_reactive_update
|
||||
let content: HTMLDivElement;
|
||||
|
||||
// Full history of chat messages in this session
|
||||
@@ -84,27 +88,25 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Flex class="h-content w-full flex-col p-8 pb-0">
|
||||
<!-- Chat Log -->
|
||||
<div
|
||||
bind:this={content}
|
||||
class:opacity-25={!Model.exists(model)}
|
||||
class="bg-medium relative mb-8 h-full w-full overflow-auto px-2"
|
||||
>
|
||||
{#each messages as message (message.id)}
|
||||
<Flex class="w-full flex-col items-start">
|
||||
<!-- Svelte hack: ensure chat is always scrolled to the bottom when a new message is added -->
|
||||
<div use:scrollToBottom class="hidden"></div>
|
||||
<Message {message} />
|
||||
</Flex>
|
||||
{/each}
|
||||
<Flex class="h-content w-full flex-col p-8 pr-2 pb-0">
|
||||
<Scrollable bind:ref={content} class="mb-8">
|
||||
<!-- Chat Log -->
|
||||
<div class:opacity-25={!Model.exists(model)} class="bg-medium relative h-full w-full px-2">
|
||||
{#each messages as message (message.id)}
|
||||
<Flex id="messages" class="w-full flex-col items-start">
|
||||
<!-- Svelte hack: ensure chat is always scrolled to the bottom when a new message is added -->
|
||||
<div use:scrollToBottom class="hidden"></div>
|
||||
<Message {message} />
|
||||
</Flex>
|
||||
{/each}
|
||||
|
||||
{#if loading}
|
||||
<Flex class="border-light h-12 w-24 rounded-lg text-center">
|
||||
<div id="loading" class="m-auto"></div>
|
||||
</Flex>
|
||||
{/if}
|
||||
</div>
|
||||
{#if loading}
|
||||
<Flex class="border-light h-12 w-24 rounded-lg text-center">
|
||||
<div id="loading" class="m-auto"></div>
|
||||
</Flex>
|
||||
{/if}
|
||||
</div>
|
||||
</Scrollable>
|
||||
|
||||
<!-- Input Box -->
|
||||
<textarea
|
||||
|
||||
31
src/components/Deleteable.svelte
Normal file
31
src/components/Deleteable.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import Flex from './Flex.svelte';
|
||||
|
||||
const { children, ondelete } = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let width = $derived(open ? 'w-24' : 'w-0');
|
||||
|
||||
function onwheel(e: WheelEvent) {
|
||||
if (e.deltaX < 0) {
|
||||
open = true;
|
||||
} else if (e.deltaX > 0) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Flex {onwheel} class="h-full w-full">
|
||||
<div class="grow">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={ondelete}
|
||||
class={`${width} bg-red transition-[width 1s linear]
|
||||
h-full justify-center overflow-hidden text-xs font-semibold
|
||||
tracking-wider text-white uppercase duration-150 hover:cursor-pointer`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</Flex>
|
||||
@@ -1,10 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const { children, class: cls, ...rest }: SvelteHTMLElements['div'] = $props();
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
ref?: HTMLDivElement;
|
||||
}
|
||||
|
||||
let { children, ref = $bindable(), class: cls, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class={twMerge('flex flex-row items-center', cls?.toString())} {...rest}>
|
||||
<div bind:this={ref} class={twMerge('flex flex-row items-center', cls?.toString())} {...rest}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
15
src/components/Icons/Pulse.svelte
Normal file
15
src/components/Icons/Pulse.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
id="fi_2875045"
|
||||
enable-background="new 0 0 48 48"
|
||||
height="100%"
|
||||
viewBox="0 0 48 48"
|
||||
width="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="m33.572 48c-.027 0-.054-.002-.081-.002-.707-.04-1.287-.578-1.377-1.28l-4.715-36.491-5.154 24.357c-.138.65-.696 1.129-1.362 1.163-.689.027-1.271-.384-1.473-1.018l-5.143-15.991-3.658 8.167c-.237.528-.761.868-1.341.868h-7.799c-.811.001-1.469-.656-1.469-1.466 0-.812.658-1.47 1.469-1.47h6.847l4.835-10.798c.249-.555.815-.889 1.421-.867.608.034 1.133.438 1.318 1.018l4.625 14.377 5.798-27.401c.147-.7.761-1.201 1.496-1.164.715.029 1.307.569 1.398 1.28l4.802 37.182 3.073-12.699c.158-.659.75-1.124 1.427-1.124h8.021c.812 0 1.47.658 1.47 1.469s-.658 1.47-1.47 1.47h-6.862l-4.669 19.295c-.16.662-.754 1.125-1.427 1.125z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 870 B |
31
src/components/Icons/Smithery.svelte
Normal file
31
src/components/Icons/Smithery.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="70.72 300.83 634.25 467.76"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
fill="#80350B"
|
||||
transform="scale(0.78125 1)"
|
||||
d="M398.006 343.355C407.196 334.657 416.38 324.018 425.476 314.838C428.882 311.4 432.24 307.705 436.078 304.748C439.729 301.935 442.869 301.26 447.393 301.077C458.675 300.618 470.141 301.016 481.437 301.027L545.114 301.034L741.615 301.026L847.247 301.065L875.393 300.962C880.452 300.938 885.928 300.517 890.933 301.3C894.414 301.845 897.551 303.124 899.612 306.102C901.107 308.262 901.765 310.809 901.994 313.397C902.705 321.454 901.877 330.173 901.885 338.3C901.892 345.547 903.023 372.202 901.814 377.303C901.183 379.968 899.785 382.164 897.71 383.944C893.722 387.363 888.202 389.596 883.542 391.984C871.992 397.902 859.627 403.282 848.499 409.9L848.073 410.157L738.736 465.592L695.547 487.185C687.28 491.325 678.652 495.199 670.71 499.919C664.916 518.373 660.554 537.457 655.651 556.169C653.713 563.563 652.127 571.537 649.466 578.688C653.06 584.966 667.316 597.326 673.383 603.24Q695.288 624.778 717.412 646.093C724.913 653.371 732.94 662.461 740.906 668.946C745.727 675.878 761.741 686.104 765.031 693.546C765.799 695.283 765.878 697.581 766.001 699.471C766.746 710.904 766.187 722.699 766.194 734.18C766.198 741.676 767.155 751.371 765.781 758.526C765.361 760.714 764.074 762.606 762.603 764.227C759.676 767.451 754.723 768.136 750.563 768.365C742.248 768.822 733.748 768.451 725.414 768.438L679.483 768.413L535.035 768.338L407.834 768.383L366.999 768.376C359.767 768.37 352.284 768.701 345.077 768.208C342.102 768.005 339.267 767.052 337.073 764.965C333.282 761.36 333.078 757.439 333.038 752.532C332.959 742.859 331.815 698.83 334.302 692.826C336.232 688.166 436.064 590.453 447.539 578.778Q424.034 541.238 400.079 503.983C386.56 503.13 373.142 502.198 359.735 500.21C268.635 486.703 199.609 447.05 129.303 389.52C121.593 383.211 96.6913 363.709 91.9741 357.005C90.7682 355.292 90.2229 352.751 90.6946 350.701C91.4452 347.438 94.1393 345.732 96.8104 344.108C108.413 343.721 120.084 343.954 131.697 343.946L192.547 343.9L398.006 343.355Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#EA580C"
|
||||
transform="scale(0.78125 1)"
|
||||
d="M446.373 302.25L830.155 302.663C836.52 302.663 890.584 302.002 892.026 302.63C896.309 304.495 897.495 305.256 900.294 308.952C900.307 318.967 902.163 372.798 900.089 378.97C899.228 381.533 895.965 383.806 893.814 385.193C886.17 390.121 877.071 393.781 868.929 397.886Q843.449 410.664 818.246 423.98L731.002 468.053C712.034 477.553 691.964 486.543 673.561 496.985C671.576 498.112 669.766 499.217 668.229 500.942C639.622 501.823 610.837 501.177 582.205 501.177L438.806 501.185L438.688 500.335C436.869 486.894 437.985 468.716 437.982 454.624L437.983 365.793C437.984 360.397 437.514 305.492 438.513 304.442C440.279 302.585 443.959 302.554 446.373 302.25Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#EA580C"
|
||||
transform="scale(0.78125 1)"
|
||||
d="M448.528 578.261C477.404 577.594 506.481 578.256 535.377 578.267C571.021 578.281 606.721 577.767 642.356 578.161C643.582 578.174 646.125 578.051 647.132 578.688C650.325 580.706 654.137 586.05 657.021 588.937L686.682 618.148C703.105 634.41 719.882 650.406 736.493 666.485C744.02 673.771 751.752 680.894 759.043 688.42C760.835 690.269 763.735 692.978 764.402 695.484C765.809 700.779 764.889 709.856 764.889 715.539Q764.959 737.209 764.727 758.877C763.82 760.43 762.917 762.081 761.622 763.35C760.108 764.835 757.92 766.183 755.798 766.514C747.092 767.874 733.038 766.875 723.684 766.875L655.026 766.867L654.998 695.298L640.005 680.333C635.648 675.883 631.344 671.04 626.591 667.026L528.906 667.004C510.258 667.001 491.575 666.754 472.932 667.119C469.439 669.957 466.388 673.702 463.199 676.896C457.43 682.676 451.344 688.161 445.954 694.306L445.99 766.863C411.397 766.874 376.689 767.494 342.11 766.721C338.59 764.815 336.342 762.935 335.005 759.061C332.875 752.89 334.318 738.619 334.286 731.536C334.251 723.765 333.026 699.896 335.241 694.21C336.495 690.99 340.183 687.533 342.525 685.047C349.459 677.687 357.06 670.841 364.222 663.683L427.583 600.731C433.544 594.767 445.04 585.538 448.528 578.261Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#EA580C"
|
||||
transform="scale(0.78125 1)"
|
||||
d="M388.873 344.75Q394.189 344.761 399.504 344.63L399.569 502.409C338.237 501.265 275.411 481.512 221.814 452.294C207.162 444.307 193.555 435.016 179.416 426.248C157.071 409.953 135.549 393.124 114.282 375.426C107.455 369.744 98.4219 363.592 93.1983 356.471C91.9835 354.814 91.4477 353.164 91.901 351.133C92.5286 348.32 95.3443 346.545 97.6458 345.247L302.403 345.232L360.341 345.197C369.535 345.186 379.836 346.102 388.873 344.75Z"
|
||||
></path>
|
||||
</g>
|
||||
<g></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
51
src/components/Input.svelte
Normal file
51
src/components/Input.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import Flex from '$components/Flex.svelte';
|
||||
|
||||
interface Props extends HTMLInputAttributes {
|
||||
ref?: HTMLInputElement;
|
||||
label: string | boolean;
|
||||
required?: boolean;
|
||||
validate?: (value: string) => boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
name,
|
||||
class: cls = '',
|
||||
required = false,
|
||||
ref = $bindable(),
|
||||
value = $bindable(),
|
||||
validate = () => true,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
let valid = $state(true);
|
||||
</script>
|
||||
|
||||
<Flex class="w-full flex-col items-start">
|
||||
{#if label}
|
||||
<label class="text-medium mt-2 mb-1 ml-2" for={name}>
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-red">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
<input
|
||||
class={twMerge(
|
||||
'border-light w-full rounded-md border p-2 px-4 outline-none',
|
||||
cls?.toString()
|
||||
)}
|
||||
class:border-red={!valid}
|
||||
onkeyup={() => (valid = validate(value))}
|
||||
onblur={() => (valid = validate(value))}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
{name}
|
||||
bind:value
|
||||
{...rest}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -26,7 +26,7 @@
|
||||
</Titlebar>
|
||||
|
||||
<section
|
||||
class="w-content-minus-nav fixed top-[var(--height-titlebar)] left-0 ml-[var(--width-nav)] h-full min-h-screen overflow-y-scroll"
|
||||
class="w-content-minus-nav fixed top-[var(--height-titlebar)] left-0 ml-[var(--width-nav)] h-full min-h-screen"
|
||||
>
|
||||
{@render children?.()}
|
||||
</section>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
|
||||
type Props = SvelteHTMLElements['a'] & {
|
||||
type Props = HTMLAttributes<HTMLAnchorElement> & {
|
||||
href: string;
|
||||
prefix?: string;
|
||||
activeClass?: string;
|
||||
@@ -11,20 +12,22 @@
|
||||
|
||||
const { children, class: cls = '', href, prefix, activeClass, ...rest }: Props = $props();
|
||||
|
||||
const path = page.url.pathname;
|
||||
const isActive = path == href || (prefix && path.startsWith(prefix));
|
||||
let isActive = $state(false);
|
||||
|
||||
afterNavigate(() => {
|
||||
const path = page.url.pathname;
|
||||
const prefixMatch = path.startsWith(href);
|
||||
const explicitPrefixMatch = path.startsWith(prefix as string);
|
||||
const pathMatch = path == href;
|
||||
isActive = pathMatch || prefixMatch || explicitPrefixMatch;
|
||||
});
|
||||
</script>
|
||||
|
||||
<a
|
||||
{...rest}
|
||||
{href}
|
||||
class={twMerge('border-l border-transparent', cls?.toString(), isActive ? activeClass : '')}
|
||||
class={twMerge('block duration-300', cls?.toString(), isActive ? activeClass : '')}
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
</style>
|
||||
|
||||
40
src/components/List.svelte
Normal file
40
src/components/List.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" generics="Item">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import Flex from './Flex.svelte';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
items: Item[];
|
||||
itemView: Snippet<[Item]>;
|
||||
borderless?: boolean;
|
||||
title?: string;
|
||||
titleClass?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
items,
|
||||
itemView,
|
||||
class: cls = '',
|
||||
titleClass = '',
|
||||
borderless = false,
|
||||
title,
|
||||
}: Props = $props();
|
||||
|
||||
let css = borderless ? '' : 'border-b-light border-b';
|
||||
</script>
|
||||
|
||||
<Flex class={twMerge('w-full flex-col items-start', cls?.toString())}>
|
||||
{#if title}
|
||||
<p class={twMerge('text-medium text-sm', titleClass?.toString())}>
|
||||
{title}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#each items as item, i (i)}
|
||||
<div class={twMerge('w-full', css)}>
|
||||
{@render itemView(item)}
|
||||
</div>
|
||||
{/each}
|
||||
</Flex>
|
||||
101
src/components/McpServer.svelte
Normal file
101
src/components/McpServer.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { constantCase } from 'change-case';
|
||||
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Input from '$components/Input.svelte';
|
||||
import Svg from '$components/Svg.svelte';
|
||||
import McpServer, { type IMcpServer } from '$lib/models/mcp-server';
|
||||
|
||||
interface Props {
|
||||
server: IMcpServer;
|
||||
}
|
||||
|
||||
interface Env {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
let { server }: Props = $props();
|
||||
|
||||
let command: string = $state('');
|
||||
let env: Env[] = $state([]);
|
||||
|
||||
let newKey: string = $state('');
|
||||
let newValue: string = $state('');
|
||||
|
||||
// Derive the computed `command` and mutable `env` from `server`. Must
|
||||
// happen before the UI renders (via `$effect.raw`) so that we use the most
|
||||
// recently set `server`.
|
||||
$effect.pre(() => {
|
||||
command = `${server.command} ${server.args.join(' ')}`.trim();
|
||||
env = Object.entries(server.env).map(([key, value]) => ({
|
||||
key: constantCase(key),
|
||||
value,
|
||||
}));
|
||||
});
|
||||
|
||||
async function save() {
|
||||
const cmd = command.split(' ');
|
||||
server.command = cmd[0];
|
||||
server.args = cmd.slice(1);
|
||||
server.env = Object.fromEntries(env.map((e) => [constantCase(e.key), e.value]));
|
||||
server = await McpServer.save(server);
|
||||
}
|
||||
|
||||
async function addEnv() {
|
||||
env.push({ key: newKey, value: newValue });
|
||||
await save();
|
||||
|
||||
newKey = '';
|
||||
newValue = '';
|
||||
}
|
||||
|
||||
async function remove(key: string) {
|
||||
env = env.filter((e) => e.key !== key);
|
||||
await save();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Flex class="w-full flex-col items-start">
|
||||
<h1 class="text-purple mb-4 ml-4 text-2xl">{server.name}</h1>
|
||||
|
||||
<h2 class="text-medium mt-8 mb-4 ml-4 text-xl">Command</h2>
|
||||
<Input
|
||||
bind:value={command}
|
||||
label={false}
|
||||
name="command"
|
||||
class="w-full"
|
||||
onchange={save}
|
||||
placeholder="uvx | npx COMMAND [args]"
|
||||
/>
|
||||
|
||||
<h2 class="text-medium mt-8 mb-4 ml-4 text-xl">Env</h2>
|
||||
<Flex class="grid w-full auto-cols-max auto-rows-max grid-cols-2 gap-4">
|
||||
{#each env as entry (entry.key)}
|
||||
<Input
|
||||
onchange={save}
|
||||
label={false}
|
||||
placeholder="Key"
|
||||
bind:value={entry.key}
|
||||
class="uppercase"
|
||||
/>
|
||||
|
||||
<Flex>
|
||||
<Input onchange={save} label={false} placeholder="Value" bind:value={entry.value} />
|
||||
<button
|
||||
class="text-dark hover:text-red ml-4 transition duration-300 hover:cursor-pointer"
|
||||
onclick={() => remove(entry.key)}
|
||||
>
|
||||
<Svg name="Delete" class="h-4 w-4" />
|
||||
</button>
|
||||
</Flex>
|
||||
{/each}
|
||||
|
||||
<Input bind:value={newKey} label={false} placeholder="Key" class="uppercase" />
|
||||
|
||||
<Flex>
|
||||
<Input bind:value={newValue} onchange={addEnv} label={false} placeholder="Value" />
|
||||
<div class="ml-4 h-4 w-4"></div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -15,7 +15,7 @@
|
||||
<Thought thought={message.thought} />
|
||||
{/if}
|
||||
|
||||
<Flex class="text-medium mb-6 w-full justify-between p-2 text-xs">
|
||||
<Flex class="assistant text-medium mb-6 w-full justify-between p-2 text-xs">
|
||||
<p class="message markdown-body text-sm whitespace-normal">
|
||||
{@html markdown.render(message.content)}
|
||||
</p>
|
||||
|
||||
30
src/components/Modal.svelte
Normal file
30
src/components/Modal.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import closables from '$lib/closables';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
close: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
const { children, close, class: cls = '' }: Props = $props();
|
||||
let ref: HTMLDivElement;
|
||||
|
||||
$effect(() => {
|
||||
closables.register(ref, close);
|
||||
return () => closables.unregister(ref);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="fixed top-0 left-0 z-40 h-screen w-screen bg-black/90"></div>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={twMerge(
|
||||
'border-light bg-dark fixed top-[50%] left-[50%] z-50 max-h-3/4 w-[400px] -translate-[50%] overflow-y-scroll rounded-xl border p-8',
|
||||
cls?.toString()
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -18,17 +18,16 @@
|
||||
This link MUST NOT preload data on hover, since it redirects when it does.
|
||||
Which means we'd navigate on hover instead of click.
|
||||
-->
|
||||
<Link
|
||||
href="/chat/latest"
|
||||
aria-label="chat"
|
||||
prefix="/chat"
|
||||
activeClass="text-purple"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<Link href="/chat/latest" aria-label="chat" prefix="/chat" activeClass="text-purple">
|
||||
<Svg name="Chat" />
|
||||
</Link>
|
||||
|
||||
<Link href="/mcp-servers" aria-label="mcp-servers" activeClass="text-purple">
|
||||
<Link
|
||||
href="/mcp-servers"
|
||||
aria-label="mcp-servers"
|
||||
prefix="/mcp-servers"
|
||||
activeClass="text-purple"
|
||||
>
|
||||
<Svg name="MCP" />
|
||||
</Link>
|
||||
|
||||
|
||||
26
src/components/Scrollable.svelte
Normal file
26
src/components/Scrollable.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import Flex from '$components/Flex.svelte';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
ref?: HTMLDivElement;
|
||||
}
|
||||
|
||||
let { children, ref = $bindable(), class: cls = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Flex bind:ref class={twMerge('h-full w-full items-start overflow-auto', cls?.toString())}>
|
||||
<div class="h-full w-full">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Scrollbars, by default, are _inside_ the padding of an element. Which
|
||||
means it will always overlap the content within. This looks shitty, so force
|
||||
the scrollable view to have extra space on the right by shoving a div in
|
||||
there.
|
||||
-->
|
||||
<div class="h-full w-6"></div>
|
||||
</Flex>
|
||||
49
src/components/Smithery/Card.svelte
Normal file
49
src/components/Smithery/Card.svelte
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import Box from '$components/Box.svelte';
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Svg from '$components/Svg.svelte';
|
||||
import type { CompactServer } from '$lib/smithery';
|
||||
|
||||
interface Props {
|
||||
server: CompactServer;
|
||||
onInstall: (server: CompactServer) => void | Promise<void>;
|
||||
}
|
||||
|
||||
const { server, onInstall }: Props = $props();
|
||||
|
||||
let close: HTMLButtonElement;
|
||||
|
||||
function trunc(text: string, chars: number) {
|
||||
return text.substring(0, chars - 3) + '...';
|
||||
}
|
||||
|
||||
async function install(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
await onInstall(server);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Box class="h-full flex-col items-start px-6">
|
||||
<a href={`https://smithery.ai/server/${server.qualifiedName}`} target="_blank">
|
||||
<h2>{server.displayName}</h2>
|
||||
<p class="text-medium">{trunc(server.description, 80)}</p>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
<Flex class="mt-2 w-full justify-between">
|
||||
<Flex title="Use Count" class="text-medium text-xs">
|
||||
<Svg name="Pulse" class="text-yellow mr-2 h-4 w-4" />
|
||||
{server.useCount}
|
||||
</Flex>
|
||||
<button
|
||||
bind:this={close}
|
||||
onclick={install}
|
||||
class="border-light hover:text-purple hover:border-purple self-end
|
||||
rounded-md border p-2 px-6 text-sm transition duration-300
|
||||
hover:cursor-pointer"
|
||||
>
|
||||
Install
|
||||
</button>
|
||||
</Flex>
|
||||
</a>
|
||||
</Box>
|
||||
170
src/components/Smithery/Configuration.svelte
Normal file
170
src/components/Smithery/Configuration.svelte
Normal file
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import Button from '$components/Button.svelte';
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Input from '$components/Input.svelte';
|
||||
import Modal from '$components/Modal.svelte';
|
||||
import { debug, info } from '$lib/logger';
|
||||
import type { McpConfig } from '$lib/mcp';
|
||||
import type { Config, ConfigSchema, Server } from '$lib/smithery';
|
||||
|
||||
interface Props {
|
||||
server: Server;
|
||||
config: ConfigSchema;
|
||||
onCancel: () => void | Promise<void>;
|
||||
onInstall: (config: McpConfig) => void | Promise<void>;
|
||||
}
|
||||
|
||||
const { server, config: _config, onCancel, onInstall }: Props = $props();
|
||||
|
||||
let config: Config[] = $state(generateConfig(_config));
|
||||
let srcdoc: string = $state('');
|
||||
let optionalIsOpen = $state(false);
|
||||
|
||||
function generateSrcdoc(server: Server, config: Config[]) {
|
||||
const { connections } = server;
|
||||
const fn = connections.findBy('type', 'stdio')?.stdioFunction || '() => ({})';
|
||||
|
||||
return `<script>
|
||||
const fn = ${fn};
|
||||
const config = fn(${serialize(config)});
|
||||
const message = { type: 'SmitheryInstall', config: config };
|
||||
window.parent.postMessage(message, '*');
|
||||
<\/script>`; // eslint-disable-line
|
||||
}
|
||||
|
||||
// Transform a raw JSONSchema object from Smithery into a simpler object we
|
||||
// can use in the UI.
|
||||
//
|
||||
function generateConfig(config: ConfigSchema): Config[] {
|
||||
if (!config.properties || Object.keys(config.properties).length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(config.properties)
|
||||
.sort(([key, _]) => (config.required.includes(key) ? -1 : 1))
|
||||
.map(([key, prop]) => ({
|
||||
name: key,
|
||||
required: config.required?.includes(key),
|
||||
description: prop.description,
|
||||
value: String(prop.default || ''),
|
||||
valid: true,
|
||||
}));
|
||||
}
|
||||
|
||||
function install() {
|
||||
validateAll();
|
||||
|
||||
if (isValid()) {
|
||||
srcdoc = generateSrcdoc(server, config);
|
||||
}
|
||||
}
|
||||
|
||||
function isValid() {
|
||||
return config.every((prop) => prop.valid);
|
||||
}
|
||||
|
||||
function validateAll() {
|
||||
config
|
||||
.filter((c) => c.required)
|
||||
.forEach((prop) => {
|
||||
prop.valid = validate(prop.value);
|
||||
});
|
||||
}
|
||||
|
||||
function validate(value: string): boolean {
|
||||
return value !== '';
|
||||
}
|
||||
|
||||
// JSON representation of the object we pass to the Smithery configuration
|
||||
// function.
|
||||
//
|
||||
function serialize(config: Config[]) {
|
||||
return JSON.stringify(convert(config));
|
||||
}
|
||||
|
||||
// Convert a `Config` to a plain `{key: value}` we can pass to the Smithery
|
||||
// configuration function.
|
||||
//
|
||||
function convert(config: Config[]) {
|
||||
const entries = config.map(({ name, value }) => [name, value]);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
function toggleOptional() {
|
||||
optionalIsOpen = optionalIsOpen ? false : true;
|
||||
}
|
||||
|
||||
// Event handler for when the Smithery config function is ran inside the
|
||||
// iFrame.
|
||||
//
|
||||
// The only way to communicate up the chain, from an iframe, is via
|
||||
// messages. The iframe does this once it figures out the config.
|
||||
//
|
||||
async function onMessage(event: MessageEvent) {
|
||||
info(event);
|
||||
const config = event.data;
|
||||
|
||||
if (config.type !== 'SmitheryInstall') {
|
||||
return;
|
||||
}
|
||||
|
||||
await onInstall(event.data.config);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onmessage={onMessage} />
|
||||
|
||||
{#if srcdoc}
|
||||
<iframe title="stdioFunction" class="hidden h-0 w-0" sandbox="allow-scripts" allow="" {srcdoc}>
|
||||
</iframe>
|
||||
{/if}
|
||||
|
||||
{#if server && config}
|
||||
<Modal close={onCancel} class="!p-0 !pb-8">
|
||||
<Flex class="w-full flex-col items-start">
|
||||
<h2 class="p-8 py-4">{server.displayName}</h2>
|
||||
|
||||
{#if config.length > 0}
|
||||
<div class="w-full px-8">
|
||||
{#each config.filter((p) => p.required) as prop (prop.name)}
|
||||
<Input
|
||||
label={prop.name}
|
||||
name={prop.name}
|
||||
bind:value={prop.value}
|
||||
required={prop.required}
|
||||
{validate}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if config.some((p) => !p.required)}
|
||||
<Flex class="mt-8 w-full flex-col items-start">
|
||||
<button
|
||||
onclick={() => toggleOptional()}
|
||||
class="text-medium text-sm-full w-full px-8 pb-2 text-left hover:cursor-pointer"
|
||||
>
|
||||
{optionalIsOpen ? '⏷' : '⏵'} Optional
|
||||
</button>
|
||||
|
||||
{#if optionalIsOpen}
|
||||
<div class="w-full px-8">
|
||||
{#each config.filter((p) => !p.required) as prop (prop.name)}
|
||||
<Input
|
||||
label={prop.name}
|
||||
name={prop.name}
|
||||
bind:value={prop.value}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Flex>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Flex class="mt-8 mr-8 self-end">
|
||||
<Button onclick={onCancel} class="border-medium text-medium mr-4">Cancel</Button>
|
||||
<Button onclick={install} class="border-purple text-purple">Install</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Modal>
|
||||
{/if}
|
||||
50
src/components/Spinner.svelte
Normal file
50
src/components/Spinner.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const { class: cls = '' } = $props();
|
||||
</script>
|
||||
|
||||
<div class={twMerge('spinner', cls?.toString())}></div>
|
||||
|
||||
<style>
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
.spinner::before {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #fff;
|
||||
animation: prixClipFix 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes prixClipFix {
|
||||
0% {
|
||||
clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0);
|
||||
}
|
||||
25% {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0);
|
||||
}
|
||||
50% {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%);
|
||||
}
|
||||
75% {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%);
|
||||
}
|
||||
100% {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Svg from './Svg.svelte';
|
||||
|
||||
const { thought } = $props();
|
||||
let isOpen = $state(false);
|
||||
|
||||
function toggle() {
|
||||
isOpen = isOpen ? false : true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<thought class="text-medium border-light mb-4 w-full rounded-xl border">
|
||||
<button
|
||||
onclick={() => toggle()}
|
||||
class="flex w-full items-center gap-4 p-2 px-4 hover:cursor-pointer"
|
||||
>
|
||||
<Svg name="Lightbulb" class="h-6 w-6" />
|
||||
Model Thoughts
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<p class="p-4 px-6 whitespace-pre-wrap">
|
||||
{thought}
|
||||
</p>
|
||||
{/if}
|
||||
</thought>
|
||||
@@ -1,10 +1,11 @@
|
||||
import '$lib/ext';
|
||||
|
||||
import type { ClientInit } from "@sveltejs/kit";
|
||||
import type { ClientInit, HandleClientError } from "@sveltejs/kit";
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { WELCOME_AGREED } from "$lib/const";
|
||||
import Config from "$lib/config";
|
||||
import * as llm from '$lib/llm';
|
||||
import { OllamaClient } from '$lib/llm';
|
||||
import { info } from '$lib/logger';
|
||||
import App from "$lib/models/app";
|
||||
import McpServer from "$lib/models/mcp-server";
|
||||
@@ -26,7 +27,7 @@ export const init: ClientInit = async () => {
|
||||
|
||||
info('[green]✔ database synced');
|
||||
|
||||
const client = new llm.Client();
|
||||
const client = new OllamaClient();
|
||||
|
||||
await startup.addCheck(
|
||||
StartupCheck.Ollama,
|
||||
@@ -44,3 +45,7 @@ export const init: ClientInit = async () => {
|
||||
async () => await Config.get(WELCOME_AGREED) === true,
|
||||
);
|
||||
}
|
||||
|
||||
export const handleError: HandleClientError = async () => {
|
||||
goto('/error');
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
type Closable = (() => Promise<void>) | (() => void);
|
||||
|
||||
// List of callables to invoke
|
||||
// eslint-disable-next-line
|
||||
const closables: Array<[any, Closable]> = [];
|
||||
let closables: Array<[Node, Closable]> = [];
|
||||
|
||||
// Manage a list of functions that should be called when the window is clicked.
|
||||
//
|
||||
@@ -13,13 +12,17 @@ const closables: Array<[any, Closable]> = [];
|
||||
// Triggered by `routes/+layout.svelte`
|
||||
//
|
||||
export default {
|
||||
register(ele: any, fn: Closable) { // eslint-disable-line
|
||||
register(ele: Node, fn: Closable) {
|
||||
closables.push([ele, fn]);
|
||||
},
|
||||
|
||||
unregister(ele: Node) {
|
||||
closables = closables.filter(([e, _]) => ele !== e);
|
||||
},
|
||||
|
||||
close(e: Event) {
|
||||
closables.forEach(([ele, fn]) => {
|
||||
if (e.target !== ele) {
|
||||
if (!ele.contains?.(e.target as Node)) {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import * as llm from "$lib/llm";
|
||||
import { type LlmOptions, OllamaClient } from "./llm";
|
||||
|
||||
import App from "$lib/models/app";
|
||||
import Message, { type IMessage } from "$lib/models/message";
|
||||
import Session, { type ISession } from "$lib/models/session";
|
||||
|
||||
export async function dispatch(session: ISession, model: string, prompt?: string): Promise<IMessage> {
|
||||
const app = App.find(session.appId as number);
|
||||
const client = new llm.Client();
|
||||
const client = new OllamaClient();
|
||||
|
||||
if (!app) {
|
||||
throw "Missing app";
|
||||
@@ -27,7 +28,7 @@ export async function dispatch(session: ISession, model: string, prompt?: string
|
||||
tool_calls: m.toolCalls,
|
||||
}));
|
||||
|
||||
const options: llm.Options = {
|
||||
const options: LlmOptions = {
|
||||
num_ctx: session.config.contextWindow,
|
||||
temperature: session.config.temperature,
|
||||
};
|
||||
|
||||
123
src/lib/ext.ts
123
src/lib/ext.ts
@@ -1,48 +1,55 @@
|
||||
// Javascript stdlib extensions
|
||||
//
|
||||
// This file should only include functions added to standard Javascript
|
||||
// objects. Each function needs a cooresponding type definition in
|
||||
// `src/app.d.ts`.
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
// Return the Object without k/v pairs where the value is explicitly undefined.
|
||||
//
|
||||
// Typically you'll use this after mapping a bunch of values where some may be
|
||||
// undefined.
|
||||
//
|
||||
// @example
|
||||
// ```ts
|
||||
// const compacted = Object.compact({ a: 1, b: true, c: undefined });
|
||||
// assertEq(compacted, { a: 1, b: true });
|
||||
// ```
|
||||
//
|
||||
/**
|
||||
* Javascript stdlib extensions
|
||||
*
|
||||
* This file should only include functions added to standard Javascript
|
||||
* objects. Each function needs a cooresponding type definition in
|
||||
* `src/app.d.ts`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return the Object without k/v pairs where the value is explicitly undefined.
|
||||
*
|
||||
* Typically you'll use this after mapping a bunch of values where some may be
|
||||
* undefined.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const compacted = Object.compact({ a: 1, b: true, c: undefined });
|
||||
* assertEq(compacted, { a: 1, b: true });
|
||||
* ```
|
||||
*/
|
||||
Object.compact = function <T>(o: Obj): T {
|
||||
return Object.fromEntries(
|
||||
Object.entries(o).filter(([_, v]) => v !== undefined),
|
||||
) as T;
|
||||
}
|
||||
|
||||
// Return the Object without specific k/v pairs, specified by key.
|
||||
//
|
||||
// @example
|
||||
// ```ts
|
||||
// const subobj = Object.without({a: 1, b: 2}, ['b']);
|
||||
// assertEq(subobj, {a: 1})
|
||||
// ```
|
||||
//
|
||||
/**
|
||||
* Return the Object without specific k/v pairs, specified by key.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const subobj = Object.without({a: 1, b: 2}, ['b']);
|
||||
* assertEq(subobj, {a: 1})
|
||||
* ```
|
||||
*/
|
||||
Object.without = function <T>(o: Obj, keys: string[]): T {
|
||||
return Object.fromEntries(
|
||||
Object.entries(o).filter(([k, _]) => !keys.includes(k)),
|
||||
) as T;
|
||||
}
|
||||
|
||||
// Remove, and return, a value by key
|
||||
//
|
||||
// @example
|
||||
// ```ts
|
||||
// const value = Object.remove({a: 1, b: 2}, 'b');
|
||||
// assertEq(value, 2);
|
||||
// ```
|
||||
//
|
||||
/**
|
||||
* Remove, and return, a value by key
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const value = Object.remove({a: 1, b: 2}, 'b');
|
||||
* assertEq(value, 2);
|
||||
* ```
|
||||
*/
|
||||
Object.remove = function <T>(o: Obj, key: string): T | undefined {
|
||||
if (Object.hasOwn(o, key)) {
|
||||
const value = o[key];
|
||||
@@ -51,15 +58,23 @@ Object.remove = function <T>(o: Obj, key: string): T | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort an array of objects by a specific key.
|
||||
//
|
||||
// @example
|
||||
// ```ts
|
||||
// const array = [{color: 'red'}, {color: 'blue'}];
|
||||
// const sorted = array.sortBy('color');
|
||||
// assertEq(sorted, [{color: 'blue'}, {color: 'red'}]);
|
||||
// ```
|
||||
//
|
||||
/**
|
||||
* Map over the entries of an object
|
||||
*/
|
||||
Object.map = function <T>(o: Obj, fn: (key: string, value: any) => any): T {
|
||||
return Object.fromEntries(Object.entries(o).map(([k, v]) => fn(k, v))) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort an array of objects by a specific key.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const array = [{color: 'red'}, {color: 'blue'}];
|
||||
* const sorted = array.sortBy('color');
|
||||
* assertEq(sorted, [{color: 'blue'}, {color: 'red'}]);
|
||||
* ```
|
||||
*/
|
||||
Array.prototype.sortBy = function <T extends Obj>(this: T[], key: string): T[] {
|
||||
return this.sort((a, b) => {
|
||||
if (a[key] < b[key]) return -1;
|
||||
@@ -67,3 +82,29 @@ Array.prototype.sortBy = function <T extends Obj>(this: T[], key: string): T[] {
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find by a specific key, in an array of objects.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const items = [{n: 1}, {n: 2}, {n: 3}];
|
||||
* assertEq(items.findBy('n', 2), {n: 2});
|
||||
* ```
|
||||
*/
|
||||
Array.prototype.findBy = function <T extends Obj>(this: T[], key: string, value: any): T | undefined {
|
||||
return this.find(item => item[key] == value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove undefined's from an array
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const items = [1, undefined, 2, 3];
|
||||
* assertEq(items.compact(), [1, 2, 3]);
|
||||
* ```
|
||||
*/
|
||||
Array.prototype.compact = function <T>(this: T[]): T[] {
|
||||
return this.filter(i => i !== undefined);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { info } from "$lib/logger";
|
||||
|
||||
type Response<T> = globalThis.Response | T | undefined;
|
||||
@@ -63,13 +65,15 @@ export abstract class HttpClient {
|
||||
}
|
||||
|
||||
async request(uri: string, options: Options = {}): Promise<globalThis.Response> {
|
||||
let response;
|
||||
let response: globalThis.Response | undefined;
|
||||
|
||||
const url = Object.remove(options, 'url') || this.url;
|
||||
const opt = { ...this.options, ...options };
|
||||
|
||||
try {
|
||||
response = await fetch(`${url}${uri}`, opt);
|
||||
const resp: globalThis.Response = await invoke('fetch', { url: `${url}${uri}`, options: opt });
|
||||
const { body, ...init } = resp;
|
||||
response = new globalThis.Response(body, init);
|
||||
} catch (err) {
|
||||
if (typeof err == 'string') {
|
||||
info(`${opt.method} ${url}${uri}: ${err}`);
|
||||
|
||||
26
src/lib/llm.d.ts
vendored
26
src/lib/llm.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
export interface Request {
|
||||
export interface OllamaRequest {
|
||||
model: string;
|
||||
stream: boolean;
|
||||
tools: Tool[];
|
||||
@@ -6,7 +6,7 @@ export interface Request {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
export interface OllamaResponse {
|
||||
created_at: string;
|
||||
done: boolean;
|
||||
done_reason: string;
|
||||
@@ -15,7 +15,7 @@ export interface Response {
|
||||
load_duration: number;
|
||||
message: {
|
||||
content: string;
|
||||
role: Role;
|
||||
role: LlmMessageRole;
|
||||
tool_calls: ToolCall[];
|
||||
};
|
||||
model: string;
|
||||
@@ -24,12 +24,12 @@ export interface Response {
|
||||
total_duration: number;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
export interface LlmOptions {
|
||||
num_ctx?: number;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
export interface OllamaModel {
|
||||
name: string;
|
||||
modifiedAt: string;
|
||||
size: number;
|
||||
@@ -68,21 +68,21 @@ export interface Model {
|
||||
capabilities?: string[];
|
||||
}
|
||||
|
||||
export type Tag = Pick<Model, 'name' | 'modifiedAt' | 'size' | 'digest' | 'details'>;
|
||||
export type Role = 'system' | 'tool' | 'user' | 'assistant';
|
||||
export type OllamaTag = Pick<OllamaModel, 'name' | 'modifiedAt' | 'size' | 'digest' | 'details'>;
|
||||
export type LlmMessageRole = 'system' | 'tool' | 'user' | 'assistant';
|
||||
|
||||
export interface Tags {
|
||||
export interface OllamaTags {
|
||||
models: Tag[];
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: Role;
|
||||
export interface LlmMessage {
|
||||
role: LlmMessageRole;
|
||||
content: string;
|
||||
name: string;
|
||||
tool_calls?: Record<string, any>; // eslint-disable-line
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
export interface LlmToolCall {
|
||||
function: {
|
||||
name: string;
|
||||
// We have no way of knowing what the LLM will pass
|
||||
@@ -91,7 +91,7 @@ export interface ToolCall {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
export interface LlmTool {
|
||||
type: string;
|
||||
function: {
|
||||
name: string;
|
||||
@@ -104,7 +104,7 @@ export interface Tool {
|
||||
}
|
||||
}
|
||||
|
||||
interface Property {
|
||||
interface LlmProperty {
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { info } from "./logger";
|
||||
import type { IMessage } from "./models/message";
|
||||
|
||||
import { HttpClient } from "$lib/http";
|
||||
import type { Message, Model, Options, Response, Tags, Tool } from "$lib/llm.d";
|
||||
import type {
|
||||
LlmMessage,
|
||||
LlmOptions,
|
||||
LlmTool,
|
||||
OllamaModel,
|
||||
OllamaResponse,
|
||||
OllamaTags
|
||||
} from "$lib/llm.d";
|
||||
import Setting from "$lib/models/setting";
|
||||
|
||||
export * from '$lib/llm.d';
|
||||
|
||||
export class Client extends HttpClient {
|
||||
export class OllamaClient extends HttpClient {
|
||||
options: RequestInit = {
|
||||
signal: AbortSignal.timeout(30000),
|
||||
headers: {
|
||||
@@ -18,7 +26,7 @@ export class Client extends HttpClient {
|
||||
return Setting.OllamaUrl;
|
||||
}
|
||||
|
||||
async chat(model: string, messages: Message[], tools: Tool[] = [], options: Options = {}): Promise<IMessage> {
|
||||
async chat(model: string, messages: LlmMessage[], tools: LlmTool[] = [], options: LlmOptions = {}): Promise<IMessage> {
|
||||
const body = JSON.stringify({
|
||||
model,
|
||||
messages,
|
||||
@@ -27,7 +35,7 @@ export class Client extends HttpClient {
|
||||
stream: false,
|
||||
});
|
||||
|
||||
const response = await this.post('/api/chat', { body }) as Response;
|
||||
const response = await this.post('/api/chat', { body }) as OllamaResponse;
|
||||
|
||||
let thought: string | undefined;
|
||||
let content: string = response
|
||||
@@ -53,18 +61,18 @@ export class Client extends HttpClient {
|
||||
};
|
||||
}
|
||||
|
||||
async list(): Promise<Model[]> {
|
||||
async list(): Promise<OllamaModel[]> {
|
||||
return (
|
||||
await this.get('/api/tags') as Tags
|
||||
).models as Model[];
|
||||
await this.get('/api/tags') as OllamaTags
|
||||
).models as OllamaModel[];
|
||||
}
|
||||
|
||||
async info(name: string): Promise<Model> {
|
||||
async info(name: string): Promise<OllamaModel> {
|
||||
const body = JSON.stringify({ name });
|
||||
|
||||
return (
|
||||
await this.post('/api/show', { body })
|
||||
) as Model;
|
||||
) as OllamaModel;
|
||||
}
|
||||
|
||||
async connected(): Promise<boolean> {
|
||||
@@ -74,8 +82,12 @@ export class Client extends HttpClient {
|
||||
}
|
||||
|
||||
async hasModels(): Promise<boolean> {
|
||||
if (!await this.connected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
await this.get('/api/tags', { raise: false }) as Tags
|
||||
)?.models.length > 0;
|
||||
await this.get('/api/tags') as OllamaTags
|
||||
).models.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
18
src/lib/mcp.d.ts
vendored
Normal file
18
src/lib/mcp.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface McpConfig {
|
||||
command: string;
|
||||
args: string[];
|
||||
env: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface McpTool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: McpInputSchema;
|
||||
}
|
||||
|
||||
export interface McpInputSchema {
|
||||
type: string;
|
||||
title: string;
|
||||
properties: { [k: string]: any; }; // eslint-disable-line
|
||||
required: string[];
|
||||
}
|
||||
@@ -1,27 +1,17 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import * as llm from '$lib/llm';
|
||||
import type { LlmTool } from "$lib/llm";
|
||||
import type { McpTool } from "$lib/mcp.d";
|
||||
import type { ISession } from "$lib/models/session";
|
||||
|
||||
export interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: InputSchema;
|
||||
}
|
||||
|
||||
export interface InputSchema {
|
||||
type: string;
|
||||
title: string;
|
||||
properties: { [k: string]: any; }; // eslint-disable-line
|
||||
required: string[];
|
||||
}
|
||||
export * from '$lib/mcp.d';
|
||||
|
||||
// Retrieve, and transform, tools from the MCP server, into `tools` object we
|
||||
// can send to the LLM.
|
||||
//
|
||||
export async function getMCPTools(session: ISession): Promise<llm.Tool[]> {
|
||||
export async function getMCPTools(session: ISession): Promise<LlmTool[]> {
|
||||
return (
|
||||
await invoke<Tool[]>('get_mcp_tools', { sessionId: session.id })
|
||||
await invoke<McpTool[]>('get_mcp_tools', { sessionId: session.id })
|
||||
).map(tool => {
|
||||
return {
|
||||
type: 'function',
|
||||
|
||||
24
src/lib/models.ts
Normal file
24
src/lib/models.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { info } from '$lib/logger';
|
||||
import App from '$lib/models/app';
|
||||
import McpServer from '$lib/models/mcp-server';
|
||||
import Message from '$lib/models/message';
|
||||
import Model from '$lib/models/model.svelte';
|
||||
import Session from '$lib/models/session';
|
||||
import Setting from '$lib/models/setting';
|
||||
|
||||
export { default as App, type IApp } from '$lib/models/app';
|
||||
export { type IMcpServer, default as McpServer } from '$lib/models/mcp-server';
|
||||
export { type IMessage, default as Message } from '$lib/models/message';
|
||||
export { type IModel, default as Model } from '$lib/models/model.svelte';
|
||||
export { type ISession, default as Session } from '$lib/models/session';
|
||||
export { type ISetting, default as Setting } from '$lib/models/setting';
|
||||
|
||||
export async function resync() {
|
||||
await App.sync();
|
||||
await Session.sync();
|
||||
await Message.sync();
|
||||
await McpServer.sync();
|
||||
await Setting.sync();
|
||||
await Model.sync();
|
||||
info('[green]✔ resynced');
|
||||
}
|
||||
@@ -47,17 +47,15 @@ export enum Interface {
|
||||
|
||||
|
||||
export default class App extends Model<IApp, Row>('apps') {
|
||||
static default(): IApp {
|
||||
return {
|
||||
name: 'Unknown',
|
||||
description: '',
|
||||
readme: '',
|
||||
image: '',
|
||||
interface: Interface.Chat,
|
||||
nodes: [],
|
||||
mcpServers: [],
|
||||
}
|
||||
}
|
||||
static defaults = {
|
||||
name: 'Unknown',
|
||||
description: '',
|
||||
readme: '',
|
||||
image: '',
|
||||
interface: Interface.Chat,
|
||||
nodes: [],
|
||||
mcpServers: [],
|
||||
};
|
||||
|
||||
static hasContext(app: IApp): boolean {
|
||||
return app.nodes?.find(n => n.type == NodeType.Context) !== undefined;
|
||||
|
||||
@@ -3,167 +3,192 @@ import Database from "@tauri-apps/plugin-sql";
|
||||
import { DATABASE_URL } from "$lib/const";
|
||||
import { info } from "$lib/logger";
|
||||
|
||||
// Database connection
|
||||
/**
|
||||
* Database connection
|
||||
*/
|
||||
export let db: Database;
|
||||
|
||||
// SQL rows should never include reserved columns.
|
||||
/**
|
||||
* SQL rows should never include reserved columns.
|
||||
*/
|
||||
export type ToSqlRow<Row> = Omit<Row, 'id' | 'created' | 'modified'>;
|
||||
|
||||
// Columns that should never be included in an UPDATE or INSERT query.
|
||||
/**
|
||||
* Columns that should never be included in an UPDATE or INSERT query.
|
||||
*/
|
||||
export const ReservedColumns = ['id', 'created', 'modified'];
|
||||
|
||||
// # Model
|
||||
//
|
||||
// Base of all database models. This class supplies the ORM functions required
|
||||
// to interact with the database and the "repo" pass-through layer.
|
||||
//
|
||||
// Model functionality is split by reads and writes. All reads are done from
|
||||
// the repo, while writes are done directory to the database, then synced
|
||||
// to the repo.
|
||||
//
|
||||
// ## Static Methods All the Way Down
|
||||
//
|
||||
// Since Svelte's reactivity only works on basic data structures – more or
|
||||
// less – we can't pass around instances of models. Instead, functions that
|
||||
// require an "instance" of a model needs to accept a object as an argument.
|
||||
//
|
||||
// NOTE: I'm a bit unsure of a ststic class interface is the right choice for
|
||||
// models. An alternative would be to build plain JS objects. This would remove
|
||||
// the need for all the `static` non-sense. :shrug:
|
||||
//
|
||||
// ## `Instance` Interface
|
||||
//
|
||||
// The `Instance` interface represent the object you pass around the app. They
|
||||
// are simple JS objects.
|
||||
//
|
||||
// `Instance` properties shouild reflect the interface you want to use in the
|
||||
// app. Meaning, camelCase keys and complex types (if needed).
|
||||
//
|
||||
// Foreign key properties should be optional to allow new `Instance`s to be
|
||||
// created where you don't know the associations at instantiation.
|
||||
//
|
||||
// ## `Row` Interface
|
||||
//
|
||||
// The `Row` interface represents a database row. Meaning, property types
|
||||
// should match database types as closely as possible.
|
||||
//
|
||||
// For example, if you have a datetime column, it would be returned from the
|
||||
// database as a string, so declare that property with `string`, `JSON` columns
|
||||
// are represented as a `string`, etc.
|
||||
//
|
||||
// ## Serielization / Deserialization
|
||||
//
|
||||
// [De]Serialization is handled through two functions `fromSql` and `toSql`
|
||||
// that you need to implement on your model.
|
||||
//
|
||||
// ### `fromSql`
|
||||
//
|
||||
// Converts a database row (`Row`) into an instance (`Instance`). This is where
|
||||
// you should convert fields like dates from a `string` to a `DateTime`, JSON
|
||||
// columns from a `string` to a "real" object, etc.
|
||||
//
|
||||
// This is called when objects are retrieved from the database.
|
||||
//
|
||||
// ### `toSql`
|
||||
//
|
||||
// Convert an instance (`Instance`) to a database row (`Row`). This is where
|
||||
// you should convert your complex types into simple database types. For
|
||||
// example, an object into the JSON stringify'ed version of itself.
|
||||
//
|
||||
// `toSql` should EXCLUDE properties for columns that are set automatically by
|
||||
// the database, like `id`, `created`, or `modified`.
|
||||
//
|
||||
// ## Lifecycle Callbacks
|
||||
//
|
||||
// You may implement `beforeCreate`, `afterCreate`, `beforeUpdate`, and
|
||||
// `afterUpdate`. See the documentation for these functions for more specific
|
||||
// information.
|
||||
//
|
||||
// ## Usage
|
||||
//
|
||||
// `Model` is a function. It takes two generic types and the name of the table
|
||||
// records reside within.
|
||||
//
|
||||
// ## Example
|
||||
//
|
||||
// ```ts
|
||||
// export interface IMessage {
|
||||
// userId: string;
|
||||
// content: string;
|
||||
// }
|
||||
//
|
||||
// interface Row {
|
||||
// user_id: string;
|
||||
// content: string;
|
||||
// }
|
||||
//
|
||||
// class Message extends Model<Interface, Row>('messages') {
|
||||
// static function fromSql(row: Row): Promise<IMessage> {
|
||||
// return {
|
||||
// id: row.id,
|
||||
// userId: row.user_id,
|
||||
// content: row.content,
|
||||
// created: moment.utc(row.created),
|
||||
// modified: moment.utc(row.modified),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// static function toSql(message: IMessage): Promise<ToSqlRow<Row>> {
|
||||
// return {
|
||||
// user_id: message.rowId,
|
||||
// content: message.content,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
/**
|
||||
* # Model
|
||||
*
|
||||
* Base of all database models. This class supplies the ORM functions required
|
||||
* to interact with the database and the "repo" pass-through layer.
|
||||
*
|
||||
* Model functionality is split by reads and writes. All reads are done from
|
||||
* the repo, while writes are done directory to the database, then synced
|
||||
* to the repo.
|
||||
*
|
||||
* ## Static Methods All the Way Down
|
||||
*
|
||||
* Since Svelte's reactivity only works on basic data structures – more or
|
||||
* less – we can't pass around instances of models. Instead, functions that
|
||||
* require an "instance" of a model needs to accept a object as an argument.
|
||||
*
|
||||
* NOTE: I'm a bit unsure of a ststic class interface is the right choice for
|
||||
* models. An alternative would be to build plain JS objects. This would remove
|
||||
* the need for all the `static` non-sense. :shrug:
|
||||
*
|
||||
* ## `Instance` Interface
|
||||
*
|
||||
* The `Instance` interface represent the object you pass around the app. They
|
||||
* are simple JS objects.
|
||||
*
|
||||
* `Instance` properties shouild reflect the interface you want to use in the
|
||||
* app. Meaning, camelCase keys and complex types (if needed).
|
||||
*
|
||||
* Foreign key properties should be optional to allow new `Instance`s to be
|
||||
* created where you don't know the associations at instantiation.
|
||||
*
|
||||
* ## `Row` Interface
|
||||
*
|
||||
* The `Row` interface represents a database row. Meaning, property types
|
||||
* should match database types as closely as possible.
|
||||
*
|
||||
* For example, if you have a datetime column, it would be returned from the
|
||||
* database as a string, so declare that property with `string`, `JSON` columns
|
||||
* are represented as a `string`, etc.
|
||||
*
|
||||
* ## Serielization / Deserialization
|
||||
*
|
||||
* [De]Serialization is handled through two functions `fromSql` and `toSql`
|
||||
* that you need to implement on your model.
|
||||
*
|
||||
* ### `fromSql`
|
||||
*
|
||||
* Converts a database row (`Row`) into an instance (`Instance`). This is where
|
||||
* you should convert fields like dates from a `string` to a `DateTime`, JSON
|
||||
* columns from a `string` to a "real" object, etc.
|
||||
*
|
||||
* This is called when objects are retrieved from the database.
|
||||
*
|
||||
* ### `toSql`
|
||||
*
|
||||
* Convert an instance (`Instance`) to a database row (`Row`). This is where
|
||||
* you should convert your complex types into simple database types. For
|
||||
* example, an object into the JSON stringify'ed version of itself.
|
||||
*
|
||||
* `toSql` should EXCLUDE properties for columns that are set automatically by
|
||||
* the database, like `id`, `created`, or `modified`.
|
||||
*
|
||||
* ## Lifecycle Callbacks
|
||||
*
|
||||
* You may implement `beforeCreate`, `afterCreate`, `beforeUpdate`, and
|
||||
* `afterUpdate`. See the documentation for these functions for more specific
|
||||
* information.
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* `Model` is a function. It takes two generic types and the name of the table
|
||||
* records reside within.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* export interface IMessage {
|
||||
* userId: string;
|
||||
* content: string;
|
||||
* }
|
||||
*
|
||||
* interface Row {
|
||||
* user_id: string;
|
||||
* content: string;
|
||||
* }
|
||||
*
|
||||
* class Message extends Model<Interface, Row>('messages') {
|
||||
* static function fromSql(row: Row): Promise<IMessage> {
|
||||
* return {
|
||||
* id: row.id,
|
||||
* userId: row.user_id,
|
||||
* content: row.content,
|
||||
* created: moment.utc(row.created),
|
||||
* modified: moment.utc(row.modified),
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* static function toSql(message: IMessage): Promise<ToSqlRow<Row>> {
|
||||
* return {
|
||||
* user_id: message.rowId,
|
||||
* content: message.content,
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export default function Model<Interface extends Obj, Row extends Obj>(table: string) {
|
||||
let repo: Interface[] = $state([]);
|
||||
|
||||
return class Model {
|
||||
// Reload records from the database and populate the Repository.
|
||||
//
|
||||
static defaults = {};
|
||||
|
||||
/**
|
||||
* Reload records from the database and populate the Repository.
|
||||
*/
|
||||
static async sync(): Promise<void> {
|
||||
repo = [];
|
||||
|
||||
(await this.query(`SELECT * FROM ${table}`))
|
||||
.forEach(record => this.syncOne(record));
|
||||
|
||||
info(`[green]✔ synced ${table}`);
|
||||
}
|
||||
|
||||
// Create an empty, default, object.
|
||||
//
|
||||
// Use this instead of the `new Whatever()` syntax, as we need to
|
||||
// always be passing around plain old JS objects for Svelte's
|
||||
// reactivity to work properly.
|
||||
//
|
||||
static default(): Interface {
|
||||
return {} as Interface;
|
||||
/**
|
||||
* Create an empty, default, object.
|
||||
*
|
||||
* Use this instead of the `new Whatever()` syntax, as we need to
|
||||
* always be passing around plain old JS objects for Svelte's
|
||||
* reactivity to work properly.
|
||||
*/
|
||||
static default(defaults: Partial<Interface> = {}): Interface {
|
||||
defaults = typeof this.defaults == 'function'
|
||||
? { ...this.defaults(), ...defaults }
|
||||
: { ...this.defaults, ...defaults };
|
||||
|
||||
return defaults as Interface;
|
||||
}
|
||||
|
||||
// Does a record with specific params exist.
|
||||
//
|
||||
/**
|
||||
* Does a record with specific params exist.
|
||||
*/
|
||||
static exists(params: Partial<Interface>): boolean {
|
||||
return this.findBy(params).length > 0;
|
||||
return this.where(params).length > 0;
|
||||
}
|
||||
|
||||
// Retrieve all records.
|
||||
//
|
||||
/**
|
||||
* Retrieve all records.
|
||||
*/
|
||||
static all(): Interface[] {
|
||||
return repo;
|
||||
}
|
||||
|
||||
// Find an individual record by `id`.
|
||||
//
|
||||
/**
|
||||
* Find an individual record by`id`.
|
||||
*/
|
||||
static find(id: number | string): Interface {
|
||||
return this.all().find(m => m.id == Number(id)) as Interface;
|
||||
}
|
||||
|
||||
// Find a collection of records by a set of the model's columns.
|
||||
//
|
||||
// Note, this expects `params` to match database columns, NOT
|
||||
// properties of a associated interface.
|
||||
//
|
||||
static findBy(params: Partial<Interface>): Interface[] {
|
||||
/**
|
||||
* Find the first occurence by a subset of the model's properties.
|
||||
*/
|
||||
static findBy(params: Partial<Interface>): Interface {
|
||||
return this.where(params)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a collection of records by a set of the model's properties.
|
||||
*/
|
||||
static where(params: Partial<Interface>): Interface[] {
|
||||
return repo.filter(m => {
|
||||
return Object
|
||||
.entries(params)
|
||||
@@ -173,22 +198,25 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
});
|
||||
}
|
||||
|
||||
// Find the first record
|
||||
//
|
||||
/**
|
||||
* Find the first record
|
||||
*/
|
||||
static first(): Interface {
|
||||
return repo[0];
|
||||
}
|
||||
|
||||
// Find the last record
|
||||
//
|
||||
/**
|
||||
* Find the last record
|
||||
*/
|
||||
static last(): Interface {
|
||||
return repo[repo.length - 1];
|
||||
}
|
||||
|
||||
// Update or Create a record.
|
||||
//
|
||||
// If `params` contains `id`, it will update, otherwise create.
|
||||
//
|
||||
/**
|
||||
* Update or Create a record.
|
||||
*
|
||||
* If `params` contains `id`, it will update, otherwise create.
|
||||
*/
|
||||
static async save(params: Interface): Promise<Interface> {
|
||||
if (params.id) {
|
||||
return await this.update(params);
|
||||
@@ -197,14 +225,15 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new record.
|
||||
//
|
||||
// You may pass a subset of properties the `Interface` expects and the
|
||||
// missing properties will be filled via the `default` function.
|
||||
//
|
||||
// `id`, `created`, and `modified` values are ALWAYS ignored, since
|
||||
// they are garaunteed to be automatically set by the database.
|
||||
//
|
||||
/**
|
||||
* Create a new record.
|
||||
*
|
||||
* You may pass a subset of properties the `Interface` expects and the
|
||||
* missing properties will be filled via the `default ` function.
|
||||
*
|
||||
* `id`, `created`, and `modified` values are ALWAYS ignored, since
|
||||
* they are garaunteed to be automatically set by the database.
|
||||
*/
|
||||
static async create(_params: Partial<Interface>): Promise<Interface> {
|
||||
let row = await this.toSql(
|
||||
this.exclude(
|
||||
@@ -224,7 +253,7 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
const values = Object.values(row);
|
||||
|
||||
let instance = (await this.query(
|
||||
`INSERT INTO ${table} (${columns}) VALUES (${binds}) RETURNING *`,
|
||||
`INSERT INTO ${table} (${columns}) VALUES(${binds}) RETURNING * `,
|
||||
values,
|
||||
))[0];
|
||||
|
||||
@@ -237,10 +266,11 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Update a record.
|
||||
//
|
||||
// Only pass the columns you intend to change.
|
||||
//
|
||||
/**
|
||||
* Update a record.
|
||||
*
|
||||
* Only pass the columns you intend to change.
|
||||
*/
|
||||
static async update(_params: Interface): Promise<Interface> {
|
||||
let row = await this.toSql(
|
||||
this.exclude(_params, ReservedColumns)
|
||||
@@ -251,10 +281,10 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
|
||||
const setters = this.setters(row).join(', ');
|
||||
const values = [...Object.values(row), _params.id];
|
||||
const idBind = `$${values.length}`;
|
||||
const idBind = `$${values.length} `;
|
||||
|
||||
let instance = (await this.query(
|
||||
`UPDATE ${table} SET ${setters} WHERE id = ${idBind} RETURNING *`,
|
||||
`UPDATE ${table} SET ${setters} WHERE id = ${idBind} RETURNING * `,
|
||||
values,
|
||||
))[0];
|
||||
|
||||
@@ -267,8 +297,9 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Delete a record, by `id`.
|
||||
//
|
||||
/**
|
||||
* Delete a record, by`id`.
|
||||
*/
|
||||
static async delete(id: number): Promise<boolean> {
|
||||
const result = (
|
||||
await (await this.db()).execute(
|
||||
@@ -283,18 +314,21 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a record by a subset of columns
|
||||
*/
|
||||
static async deleteBy(params: Partial<Row>): Promise<boolean> {
|
||||
const conditions = this.setters(params).join(' AND ');
|
||||
const values = Object.values(params);
|
||||
|
||||
const instances = await this.query(
|
||||
`SELECT * FROM ${table} WHERE ${conditions}`,
|
||||
`SELECT * FROM ${table} WHERE ${conditions} `,
|
||||
values,
|
||||
);
|
||||
|
||||
const success = (
|
||||
await (await this.db()).execute(
|
||||
`DELETE FROM ${table} WHERE ${conditions}`,
|
||||
`DELETE FROM ${table} WHERE ${conditions} `,
|
||||
values,
|
||||
)
|
||||
).rowsAffected >= 1;
|
||||
@@ -308,8 +342,9 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
return success;
|
||||
}
|
||||
|
||||
// Run a query in the database, returning an object implementing `Instance`.
|
||||
//
|
||||
/**
|
||||
* Run a query in the database, returning an object implementing`Instance`.
|
||||
*/
|
||||
protected static async query(sql: string, values: unknown[] = []): Promise<Interface[]> {
|
||||
const result: Row[] = (
|
||||
await (await this.db()).select<Row[]>(sql, values)
|
||||
@@ -322,8 +357,9 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
);
|
||||
}
|
||||
|
||||
// Memoized database connection.
|
||||
//
|
||||
/**
|
||||
* Memoized database connection.
|
||||
*/
|
||||
protected static async db(): Promise<Database> {
|
||||
if (!db) {
|
||||
db = await Database.load(DATABASE_URL);
|
||||
@@ -331,8 +367,9 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
return db;
|
||||
}
|
||||
|
||||
// Update, or Add, a single record
|
||||
//
|
||||
/**
|
||||
* Update, or Add, a single record
|
||||
*/
|
||||
private static syncOne(record: Interface) {
|
||||
const existing = this.find(record.id);
|
||||
|
||||
@@ -344,32 +381,35 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
}
|
||||
}
|
||||
|
||||
// Remove an instance from the repo
|
||||
//
|
||||
/**
|
||||
* Remove an instance from the repo
|
||||
*/
|
||||
private static syncRemove(instance: Interface) {
|
||||
repo = repo.filter(
|
||||
i => i.id !== instance.id
|
||||
);
|
||||
}
|
||||
|
||||
// Remove ephemeral instances from the repo.
|
||||
//
|
||||
// Pages will often push an "empty" instance into a list of models, to
|
||||
// so that it renders in a list and the user can configure it.
|
||||
//
|
||||
// We need to remove those "ephemeral" instances when we save a record,
|
||||
// otherwise both would show up and appear to be duplicate.
|
||||
//
|
||||
// This leaves only persisted records (ones with an `id`).
|
||||
//
|
||||
/**
|
||||
* Remove ephemeral instances from the repo.
|
||||
*
|
||||
* Pages will often push an "empty" instance into a list of models, to
|
||||
* so that it renders in a list and the user can configure it.
|
||||
*
|
||||
* We need to remove those "ephemeral" instances when we save a record,
|
||||
* otherwise both would show up and appear to be duplicate.
|
||||
*
|
||||
* This leaves only persisted records(ones with an`id`).
|
||||
*/
|
||||
private static removeEphemeralInstances() {
|
||||
repo = repo.filter(
|
||||
record => record.id !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
// Exclude k/v pairs in an object, by a list of keys.
|
||||
//
|
||||
/**
|
||||
* Exclude k / v pairs in an object, by a list of keys.
|
||||
*/
|
||||
private static exclude<T extends Obj>(params: T, exclude: string[]): T {
|
||||
return Object.fromEntries(
|
||||
Object
|
||||
@@ -378,50 +418,57 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
) as T;
|
||||
}
|
||||
|
||||
// Retrieve the list of columns from `params`.
|
||||
//
|
||||
// Mostly just a more descriptive name for the operation.
|
||||
//
|
||||
/**
|
||||
* Retrieve the list of columns from`params`.
|
||||
*
|
||||
* Mostly just a more descriptive name for the operation.
|
||||
*/
|
||||
private static columns<P extends Obj>(params: P): string[] {
|
||||
return Object.keys(params);
|
||||
}
|
||||
|
||||
// Generate a list of `$k = $#` statements from `params`.
|
||||
//
|
||||
// `$k` is the name of the column and `$#` is the bind parameter.
|
||||
//
|
||||
/**
|
||||
* Generate a list of `$k = $#` statements from`params`.
|
||||
*
|
||||
* `$k` is the name of the column and `$#` is the bind parameter.
|
||||
*/
|
||||
private static setters<P extends Obj>(params: P): string[] {
|
||||
return Object.keys(params).map((k, i) => `${k} = $${i + 1}`);
|
||||
return Object.keys(params).map((k, i) => `${k} = $${i + 1} `);
|
||||
}
|
||||
|
||||
// Individual numeric bind statements, like `['$1', '$2']`.
|
||||
//
|
||||
/**
|
||||
* Individual numeric bind statements, like`['$1', '$2']`.
|
||||
*/
|
||||
private static binds<P extends Obj>(params: P): string[] {
|
||||
return Object.values(params).map((_, i) => `$${i + 1}`);
|
||||
return Object.values(params).map((_, i) => `$${i + 1} `);
|
||||
}
|
||||
|
||||
// Transform a raw database row into an `Interface` object.
|
||||
//
|
||||
/**
|
||||
* Transform a raw database row into an `Interface` object.
|
||||
*/
|
||||
protected static async fromSql(_: Row): Promise<Interface> {
|
||||
throw "NotImplementedError";
|
||||
}
|
||||
|
||||
// Transform an `Interface` object into a `Row` of database compatiable
|
||||
// values.
|
||||
//
|
||||
/**
|
||||
* Transform an `Interface` object into a `Row` of database compatiable
|
||||
* values.
|
||||
*/
|
||||
protected static async toSql(_: ToSqlRow<Interface>): Promise<ToSqlRow<Row>> {
|
||||
throw "NotImplementedError";
|
||||
}
|
||||
|
||||
// Transform the `Row` object before it's used to generate a query.
|
||||
//
|
||||
/**
|
||||
* Transform the `Row` object before it's used to generate a query.
|
||||
*/
|
||||
protected static async beforeSave(row: ToSqlRow<Row>): Promise<ToSqlRow<Row>> {
|
||||
return row;
|
||||
}
|
||||
|
||||
// Transform the `Instance` object after it's created/updated/retrieved
|
||||
// from the database.
|
||||
//
|
||||
/**
|
||||
* Transform the `Instance` object after it's created/updated/retrieved
|
||||
* from the database.
|
||||
*/
|
||||
protected static async afterSave(instance: Interface): Promise<Interface> {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -6,14 +6,20 @@ import Model, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||
|
||||
export interface IMcpServer {
|
||||
id?: number;
|
||||
name: string;
|
||||
command: string;
|
||||
metadata?: Metadata;
|
||||
args: string[];
|
||||
env: Record<string, string>;
|
||||
}
|
||||
|
||||
interface Row {
|
||||
id: number;
|
||||
name: string;
|
||||
command: string;
|
||||
metadata: string;
|
||||
args: string;
|
||||
env: string;
|
||||
}
|
||||
|
||||
interface Metadata {
|
||||
@@ -28,21 +34,22 @@ interface Metadata {
|
||||
}
|
||||
|
||||
export default class McpServer extends Model<IMcpServer, Row>('mcp_servers') {
|
||||
static default(): IMcpServer {
|
||||
return {
|
||||
command: '',
|
||||
metadata: {
|
||||
protocolVersion: '',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: undefined,
|
||||
version: '',
|
||||
}
|
||||
static defaults = {
|
||||
name: 'Unknown',
|
||||
command: '',
|
||||
metadata: {
|
||||
protocolVersion: '',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
serverInfo: {
|
||||
name: undefined,
|
||||
version: '',
|
||||
}
|
||||
},
|
||||
args: [],
|
||||
env: {},
|
||||
};
|
||||
|
||||
static async forApp(appId: number): Promise<IMcpServer[]> {
|
||||
return await this.query(
|
||||
@@ -59,6 +66,8 @@ export default class McpServer extends Model<IMcpServer, Row>('mcp_servers') {
|
||||
await invoke('start_mcp_server', {
|
||||
sessionId: session.id,
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,11 +80,21 @@ export default class McpServer extends Model<IMcpServer, Row>('mcp_servers') {
|
||||
|
||||
static async afterCreate(server: IMcpServer): Promise<IMcpServer> {
|
||||
const metadata: Metadata = JSON.parse(
|
||||
await invoke('get_metadata', { command: server.command })
|
||||
await invoke('get_metadata', {
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env,
|
||||
})
|
||||
);
|
||||
|
||||
const name = metadata
|
||||
.serverInfo
|
||||
?.name
|
||||
?.replace('mcp-server/', '') as string;
|
||||
|
||||
return await this.update({
|
||||
...server,
|
||||
name,
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
@@ -83,15 +102,21 @@ export default class McpServer extends Model<IMcpServer, Row>('mcp_servers') {
|
||||
static async fromSql(row: Row): Promise<IMcpServer> {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
command: row.command,
|
||||
metadata: JSON.parse(row.metadata),
|
||||
args: JSON.parse(row.args),
|
||||
env: JSON.parse(row.env),
|
||||
};
|
||||
}
|
||||
|
||||
static async toSql(server: IMcpServer): Promise<ToSqlRow<Row>> {
|
||||
return {
|
||||
name: server.name,
|
||||
command: server.command,
|
||||
metadata: JSON.stringify(server.metadata),
|
||||
args: JSON.stringify(server.args),
|
||||
env: JSON.stringify(server.env),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import moment from "moment";
|
||||
|
||||
import type { Role } from '$lib/llm.d';
|
||||
import type { LlmMessageRole } from '$lib/llm.d';
|
||||
import Model, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||
import Session, { type ISession } from "$lib/models/session";
|
||||
|
||||
export interface IMessage {
|
||||
id?: number;
|
||||
role: Role;
|
||||
role: LlmMessageRole;
|
||||
content: string;
|
||||
thought?: string;
|
||||
model: string;
|
||||
@@ -33,15 +33,13 @@ interface Row {
|
||||
}
|
||||
|
||||
export default class Message extends Model<IMessage, Row>('messages') {
|
||||
static default(): IMessage {
|
||||
return {
|
||||
role: 'user',
|
||||
content: '',
|
||||
model: '',
|
||||
name: '',
|
||||
toolCalls: [],
|
||||
}
|
||||
}
|
||||
static defaults = {
|
||||
role: 'user',
|
||||
content: '',
|
||||
model: '',
|
||||
name: '',
|
||||
toolCalls: [],
|
||||
};
|
||||
|
||||
static session(message: IMessage): ISession {
|
||||
return Session.find(message.sessionId as number);
|
||||
@@ -56,7 +54,7 @@ export default class Message extends Model<IMessage, Row>('messages') {
|
||||
protected static async fromSql(row: Row): Promise<IMessage> {
|
||||
return {
|
||||
id: row.id,
|
||||
role: row.role as Role,
|
||||
role: row.role as LlmMessageRole,
|
||||
content: row.content,
|
||||
thought: row.thought,
|
||||
model: row.model,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as llm from '$lib/llm';
|
||||
import { OllamaClient, type OllamaModel } from "$lib/llm";
|
||||
|
||||
export type IModel = llm.Model;
|
||||
export type IModel = OllamaModel;
|
||||
|
||||
export interface Details {
|
||||
parentModel: string;
|
||||
@@ -15,7 +15,7 @@ let repo: IModel[] = $state([]);
|
||||
|
||||
export default class Model {
|
||||
static async sync(): Promise<void> {
|
||||
const client = new llm.Client();
|
||||
const client = new OllamaClient();
|
||||
const models: IModel[] = await client.list();
|
||||
|
||||
repo = await Promise.all(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import moment from "moment";
|
||||
|
||||
import * as llm from '$lib/llm';
|
||||
import { type LlmTool, OllamaClient } from "$lib/llm";
|
||||
import { getMCPTools } from '$lib/mcp';
|
||||
import App, { type IApp } from '$lib/models/app';
|
||||
import Base, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||
@@ -33,17 +33,15 @@ interface Row {
|
||||
}
|
||||
|
||||
export default class Session extends Base<ISession, Row>('sessions') {
|
||||
static default(): ISession {
|
||||
return {
|
||||
summary: DEFAULT_SUMMARY,
|
||||
config: {
|
||||
model: Model.default().name,
|
||||
contextWindow: 4096,
|
||||
temperature: 0.8,
|
||||
enabledMcpServers: [],
|
||||
}
|
||||
static defaults = () => ({
|
||||
summary: DEFAULT_SUMMARY,
|
||||
config: {
|
||||
model: Model.default().name,
|
||||
contextWindow: 4096,
|
||||
temperature: 0.8,
|
||||
enabledMcpServers: [],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
static app(session: ISession): IApp | undefined {
|
||||
if (!session.appId) return;
|
||||
@@ -52,10 +50,10 @@ export default class Session extends Base<ISession, Row>('sessions') {
|
||||
|
||||
static messages(session: ISession): IMessage[] {
|
||||
if (!session.id) return [];
|
||||
return Message.findBy({ sessionId: session.id });
|
||||
return Message.where({ sessionId: session.id });
|
||||
}
|
||||
|
||||
static async tools(session: ISession): Promise<llm.Tool[]> {
|
||||
static async tools(session: ISession): Promise<LlmTool[]> {
|
||||
const model = Model.find(session.config.model);
|
||||
|
||||
if (!Model.supportsTools(model)) {
|
||||
@@ -105,7 +103,7 @@ export default class Session extends Base<ISession, Row>('sessions') {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new llm.Client();
|
||||
const client = new OllamaClient();
|
||||
const message: IMessage = await client.chat(
|
||||
model,
|
||||
[
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OLLAMA_URL_CONFIG_KEY } from '$lib/const';
|
||||
import * as llm from '$lib/llm';
|
||||
import { OllamaClient } from '$lib/llm';
|
||||
import Model, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||
import LLMModel from '$lib/models/model.svelte';
|
||||
|
||||
@@ -19,14 +19,12 @@ interface Row {
|
||||
|
||||
export default class Setting extends Model<ISetting, Row>('settings') {
|
||||
static get OllamaUrl(): string {
|
||||
return (
|
||||
this.findBy({ key: OLLAMA_URL_CONFIG_KEY })
|
||||
)[0].value as string;
|
||||
return this.findBy({ key: OLLAMA_URL_CONFIG_KEY }).value as string;
|
||||
}
|
||||
|
||||
static async validate(setting: ISetting): Promise<boolean> {
|
||||
if (setting.key == OLLAMA_URL_CONFIG_KEY) {
|
||||
const client = new llm.Client({ url: setting.value as string })
|
||||
const client = new OllamaClient({ url: setting.value as string })
|
||||
return await client.connected();
|
||||
}
|
||||
return true;
|
||||
|
||||
67
src/lib/smithery.d.ts
vendored
Normal file
67
src/lib/smithery.d.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
import jsonSchema from 'json-schema';
|
||||
const { JSONSchema7 } = jsonSchema;
|
||||
|
||||
export interface ServerList {
|
||||
servers: CompactServer[];
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompactServer {
|
||||
qualifiedName: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
homepage: string;
|
||||
useCount: string;
|
||||
isDeployed: boolean
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
qualifiedName: string;
|
||||
displayName: string;
|
||||
iconUrl: string | null;
|
||||
deploymentUrl: string;
|
||||
connections: Connection[];
|
||||
security?: {
|
||||
scanPassed: boolean;
|
||||
};
|
||||
tools?: Array<{
|
||||
name: string;
|
||||
description: string | null;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties?: object;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
type: 'stdio' | 'ws' | 'http';
|
||||
configSchema: ConfigSchema;
|
||||
stdioFunction: string;
|
||||
}
|
||||
|
||||
export interface ConfigSchema {
|
||||
type: 'object';
|
||||
required: string[];
|
||||
properties: {
|
||||
[key: string]: {
|
||||
type: 'string';
|
||||
default?: any; // eslint-disable-line
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
name: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
value: string;
|
||||
valid: boolean;
|
||||
}
|
||||
1
src/lib/smithery.ts
Normal file
1
src/lib/smithery.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '$lib/smithery.d';
|
||||
38
src/lib/smithery/client.ts
Normal file
38
src/lib/smithery/client.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { HttpClient } from "$lib/http";
|
||||
import type { CompactServer, Server, ServerList } from '$lib/smithery.d';
|
||||
|
||||
export * from '$lib/smithery.d';
|
||||
|
||||
export class Client extends HttpClient {
|
||||
options: RequestInit = {
|
||||
signal: AbortSignal.timeout(30000),
|
||||
headers: {
|
||||
'Authorization': 'Bearer 4f91ed5c-d3ae-4ba6-9169-10455db2e626',
|
||||
}
|
||||
};
|
||||
|
||||
get url() {
|
||||
return 'https://registry.smithery.ai';
|
||||
}
|
||||
|
||||
async servers(page: number = 1): Promise<CompactServer[]> {
|
||||
return (
|
||||
await this.get(`/servers?q=is:local&pageSize=24&page=${page}`) as ServerList
|
||||
).servers;
|
||||
}
|
||||
|
||||
async server(name: string): Promise<Server> {
|
||||
return (
|
||||
await this.get(`/servers/${name}`) as Server
|
||||
)
|
||||
}
|
||||
|
||||
async search(query: string): Promise<CompactServer[]> {
|
||||
const q = encodeURIComponent(query)
|
||||
.replace(/%20/g, '+');
|
||||
|
||||
return (
|
||||
await this.get(`/servers?q=is:local+${q}`) as ServerList
|
||||
).servers;
|
||||
}
|
||||
}
|
||||
227
src/lib/smithery/command.ts
Normal file
227
src/lib/smithery/command.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import ts from 'typescript';
|
||||
|
||||
import type { Server } from '$lib/smithery';
|
||||
|
||||
const IS_CONFIG = /^(\s*config\.)(.+)$/;
|
||||
|
||||
// A Node is an object that knows how to be evaluated using a `config` object
|
||||
// which may contain values to be interpolated.
|
||||
//
|
||||
abstract class Node {
|
||||
text: string;
|
||||
abstract eval(config: Record<string, string>): string;
|
||||
|
||||
constructor(text: string) {
|
||||
this.text = text.trim().replaceAll("'", '');
|
||||
}
|
||||
}
|
||||
|
||||
// No interpolation, just a regular string.
|
||||
//
|
||||
// `text` is the value.
|
||||
//
|
||||
class StringNode extends Node {
|
||||
eval(_: Record<string, string>) {
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
|
||||
// Gets its value from the config object passed in.
|
||||
//
|
||||
// `text` is the name of the key in `config`.
|
||||
//
|
||||
class ConfigNode extends Node {
|
||||
eval(config: Record<string, string>): string {
|
||||
return config[this.text];
|
||||
}
|
||||
}
|
||||
|
||||
// Parent object that wraps a sets of `Node`s and can give us the final
|
||||
// `Config` using those `Node`s.
|
||||
//
|
||||
export class Config {
|
||||
command: StringNode;
|
||||
args: Node[];
|
||||
env: Record<string, Node>;
|
||||
|
||||
static from(server: Server) {
|
||||
return parse(server);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.command = new StringNode('');
|
||||
this.args = [];
|
||||
this.env = {};
|
||||
}
|
||||
|
||||
eval(config: Record<string, string>) {
|
||||
return {
|
||||
command: this.command.eval(config),
|
||||
args: this.args.map(a => a.eval(config)),
|
||||
env: Object.fromEntries(
|
||||
Object.entries(this.env).map(([k, v]) => [k, v.eval(config)])
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse out the start command of an MCP server, from Smithery's configuration
|
||||
// file syntax.
|
||||
//
|
||||
// Smithery's JSON schema for MCP servers involves specifying a JS function
|
||||
// that evaluates into the final config object used to start the server
|
||||
// process.
|
||||
//
|
||||
// ```
|
||||
// startCommand:
|
||||
// ...
|
||||
// commandFunction:
|
||||
// |-
|
||||
// (config) => ({
|
||||
// "command" => "npx",
|
||||
// "args" => ["an-mcp-server", "--apiKey", config.apiKey],
|
||||
// "env": {
|
||||
// OTHER_THING: config.otherThing
|
||||
// }
|
||||
// })
|
||||
// ```
|
||||
//
|
||||
// This seems like a supremely bad idea, since it means `eval`ing arbitrary,
|
||||
// user-supplied, code. Likely on a server, since it's the parts necessary to
|
||||
// spawn a process. I could totally be missing something, though. so :shrug:
|
||||
//
|
||||
// In any case, we're not going to `eval` anything. Instead, we build an AST of
|
||||
// this code and parse out the nodes we care about. We build a `Config` object
|
||||
// that will then be "evaluated" later, when we have the values we need to
|
||||
// interpolate.
|
||||
//
|
||||
// This approach avoids any dangers of executing arbitrary code.
|
||||
//
|
||||
function parse(server: Server): Config {
|
||||
const stdio = server.connections.find(c => c.type == 'stdio');
|
||||
const cmd = stdio?.stdioFunction || '';
|
||||
const config: Config = new Config();
|
||||
|
||||
// Build an AST from the function declaration
|
||||
const source = ts.createSourceFile(
|
||||
"source.ts",
|
||||
cmd,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
);
|
||||
|
||||
// Relevant tokens to eval
|
||||
const tokens: string[] = [];
|
||||
|
||||
// Take the next token unconditionally.
|
||||
let take = false;
|
||||
|
||||
// Collect the tokens we care about from the AST
|
||||
visit(source);
|
||||
|
||||
// For each token, transform them into `Node`s.
|
||||
tokens.forEach((token, i) => {
|
||||
if (token === 'command') {
|
||||
config.command = command(tokens[i + 1]);
|
||||
} else if (token === 'args') {
|
||||
config.args = args(tokens[i + 1]);
|
||||
} else if (token === 'env') {
|
||||
config.env = env(tokens[i + 1]);
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
|
||||
// Visit a Typescript AST node and track relevant ones.
|
||||
//
|
||||
// We only care about two types of nodes – `Identifier` (that has a text
|
||||
// value of "command", "args", or "env") and next node.
|
||||
//
|
||||
function visit(node: ts.Node) {
|
||||
// Previous token was an identifier that we care about. If that's the
|
||||
// case, also track this node as it's the value of one of those objects
|
||||
if (take) {
|
||||
tokens.push(node.getText());
|
||||
}
|
||||
|
||||
// Reset this so we only record N(Identifier)+1 nodes.
|
||||
take = false;
|
||||
|
||||
// If we care about this, track it.
|
||||
if (node.kind == ts.SyntaxKind.Identifier && isNeeded(node.getText())) {
|
||||
tokens.push(node.getText());
|
||||
take = true;
|
||||
}
|
||||
|
||||
// Keep on truck'n.
|
||||
ts.forEachChild(node, (c) => visit(c));
|
||||
}
|
||||
|
||||
// Command is always just a plain string.
|
||||
//
|
||||
function command(text: string) {
|
||||
return new StringNode(text);
|
||||
}
|
||||
|
||||
// Parse the list of `args` into a list of `Node`s.
|
||||
//
|
||||
// An `arg` can either just be a plain string – "--verbose" – which will end
|
||||
// up as a `StringNode`.
|
||||
//
|
||||
// Or it can be a `config` variable – "config.apiKey" – which will end up
|
||||
// as a `ConfigNode` and be evaluated later for the final value.
|
||||
//
|
||||
function args(text: string): Node[] {
|
||||
text = text.replace('[', '').replace(']', '');
|
||||
|
||||
if (text == '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return text
|
||||
.split(',')
|
||||
.map(a => (
|
||||
IS_CONFIG.test(a)
|
||||
? new ConfigNode(a.trim().replace('config.', ''))
|
||||
: new StringNode(a)
|
||||
))
|
||||
.compact() as Node[];
|
||||
}
|
||||
|
||||
// Parse the list of `env` vars into a list of `Node`s.
|
||||
//
|
||||
// `env` entries will always have a plain string key, but can have a string
|
||||
// or config value.
|
||||
//
|
||||
// Does the same as `args` and tracks the corresponding `Node` to be
|
||||
// evaluated later.
|
||||
//
|
||||
function env(text: string): Record<string, Node> {
|
||||
text = text.replace('{', '').replace('}', '');
|
||||
|
||||
if (text === '') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
text
|
||||
.split(',')
|
||||
.map(kv => {
|
||||
let [k, v] = kv.split(':'); // eslint-disable-line
|
||||
let value: Node = new StringNode(v);
|
||||
|
||||
if (IS_CONFIG.test(v)) {
|
||||
value = new ConfigNode(v.replace('config.', ''));
|
||||
}
|
||||
|
||||
return [k, value];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// We only care about these three keys
|
||||
//
|
||||
function isNeeded(text: string) {
|
||||
return ['command', 'args', 'env'].includes(text);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export default {
|
||||
// things like sync the database, load models, etc.
|
||||
//
|
||||
async addCheck(check: StartupCheck, condition: Condition, onSuccess?: OnSuccess) {
|
||||
// Immediately run the check to see if it's already satisfied.
|
||||
if (!await condition()) {
|
||||
info(`[red]ⅹ startup check failed:[default] ${check}`);
|
||||
checks.push([check, condition, onSuccess]);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export enum Interface {
|
||||
Voice = "Voice",
|
||||
Chat = "Chat",
|
||||
Dashboard = "Dashboard",
|
||||
Daemon = "Daemon",
|
||||
}
|
||||
|
||||
export enum NodeType {
|
||||
Context = "Context",
|
||||
}
|
||||
|
||||
export interface CheckboxEvent extends Event {
|
||||
currentTarget: EventTarget & HTMLInputElement;
|
||||
}
|
||||
42
src/lib/util.svelte.ts
Normal file
42
src/lib/util.svelte.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
// Delay the execution of something until the event that triggers it, stops.
|
||||
//
|
||||
// Useful for keypress events you want to run when the user stops typing for
|
||||
// some period of time.
|
||||
//
|
||||
// @example
|
||||
// ```ts
|
||||
// <input onkeyup={debounce(doThing)} />
|
||||
// ```
|
||||
//
|
||||
export function debounce(fn: (...args: any[]) => any, timeout = 250) {
|
||||
let timer: number | undefined;
|
||||
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timer);
|
||||
|
||||
timer = setTimeout(() => {
|
||||
fn(...args);
|
||||
}, timeout) as unknown as number;
|
||||
}
|
||||
}
|
||||
|
||||
// Run a function when a specific key is pressed.
|
||||
//
|
||||
// @example
|
||||
// ```ts
|
||||
// function submit(e: KeyboardEvent) {
|
||||
// coolSubmitStuff();
|
||||
// }
|
||||
//
|
||||
// <input onkeypress={onkey('Enter', submit)} />
|
||||
// ```
|
||||
//
|
||||
export function onkey(key: string, fn: (e: KeyboardEvent) => any) {
|
||||
return (e: KeyboardEvent) => {
|
||||
if (e.key == key) {
|
||||
fn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,28 @@
|
||||
<!-- App-wide event handlers -->
|
||||
<script lang="ts">
|
||||
// App-wide event handlers
|
||||
|
||||
import closables from '$lib/closables';
|
||||
import { resync } from '$lib/models';
|
||||
|
||||
const { children } = $props();
|
||||
|
||||
async function onWindowClick(e: Event) {
|
||||
function onclick(e: Event) {
|
||||
closables.close(e);
|
||||
}
|
||||
|
||||
function onkeypress(e: KeyboardEvent) {
|
||||
if (e.key == 'Escape') {
|
||||
closables.close(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function onkeydown(e: KeyboardEvent) {
|
||||
// manually reload data from database
|
||||
if (e.metaKey && e.key == 'r') {
|
||||
await resync();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={(e) => onWindowClick(e)} />
|
||||
<svelte:window {onclick} {onkeypress} {onkeydown} />
|
||||
|
||||
{@render children?.()}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { CHAT_APP_ID } from '$lib/const';
|
||||
import Chat from '$components/Chat.svelte';
|
||||
import Deleteable from '$components/Deleteable.svelte';
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Layout from '$components/Layouts/Default.svelte';
|
||||
import Menu from '$components/Menu.svelte';
|
||||
@@ -102,35 +103,27 @@
|
||||
{/snippet}
|
||||
|
||||
<Layout {titlebar}>
|
||||
<Flex class="h-full">
|
||||
<Flex class="border-light bg-medium h-full w-[300px] flex-col border-r">
|
||||
<Flex class="h-full items-start">
|
||||
<Flex
|
||||
class="border-light bg-medium h-content w-[300px] flex-col overflow-y-scroll border-r"
|
||||
>
|
||||
{#each sessions as sess (sess.id)}
|
||||
<Flex
|
||||
class={`group text-medium border-b-light w-full justify-between border-b
|
||||
border-l-transparent pr-4 text-sm
|
||||
${sess.id == session?.id ? '!border-l-purple border-l' : ''}`}
|
||||
border-l-transparent text-sm ${sess.id == session?.id ? '!border-l-purple border-l' : ''}`}
|
||||
>
|
||||
<a
|
||||
href={`/chat/${sess.id}`}
|
||||
class:text-purple={sess.id == session.id}
|
||||
class="w-full max-w-[220px] py-3 pl-8 text-left hover:cursor-pointer"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{sess.summary}
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
icon: 'Delete',
|
||||
label: 'Delete',
|
||||
onclick: async () => await deleteSession(sess),
|
||||
style: 'text-red',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Deleteable ondelete={() => deleteSession(sess)}>
|
||||
<a
|
||||
href={`/chat/${sess.id}`}
|
||||
class:text-purple={sess.id == session.id}
|
||||
class="block w-full max-w-[220px] py-3 pl-8 text-left hover:cursor-pointer"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{sess.summary}
|
||||
</p>
|
||||
</a>
|
||||
</Deleteable>
|
||||
</Flex>
|
||||
{/each}
|
||||
</Flex>
|
||||
|
||||
11
src/routes/error/+page.svelte
Normal file
11
src/routes/error/+page.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Layout from '$components/Layouts/Default.svelte';
|
||||
import Modal from '$components/Modal.svelte';
|
||||
</script>
|
||||
|
||||
<Layout>
|
||||
<Modal close={() => {}}>
|
||||
<h1 class="text-red">Whoops. Something went wrong.</h1>
|
||||
<a href="/">Go Back</a>
|
||||
</Modal>
|
||||
</Layout>
|
||||
88
src/routes/mcp-servers/+layout.svelte
Normal file
88
src/routes/mcp-servers/+layout.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import Deleteable from '$components/Deleteable.svelte';
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Layout from '$components/Layouts/Default.svelte';
|
||||
import Link from '$components/Link.svelte';
|
||||
import List from '$components/List.svelte';
|
||||
import Svg from '$components/Svg.svelte';
|
||||
import Titlebar from '$components/Titlebar.svelte';
|
||||
import McpServer, { type IMcpServer } from '$lib/models/mcp-server';
|
||||
|
||||
interface Registry {
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const { children } = $props();
|
||||
|
||||
let mcpServers: IMcpServer[] = $derived(McpServer.all());
|
||||
let registries: Registry[] = [
|
||||
{
|
||||
name: 'Smithery',
|
||||
icon: 'Smithery',
|
||||
},
|
||||
];
|
||||
|
||||
async function destroy(server: IMcpServer) {
|
||||
await McpServer.delete(server.id as number);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet titlebar()}
|
||||
<Titlebar class="h-[60px] w-full">
|
||||
<Flex
|
||||
class=" border-r-light h-full w-[300px] items-center justify-between border-r px-8 pr-4"
|
||||
>
|
||||
<h1 class="font-[500]">MCP Servers</h1>
|
||||
<a
|
||||
href="/mcp-servers/new"
|
||||
class="border-light h-8 w-8 rounded-md border hover:cursor-pointer"
|
||||
>
|
||||
<p class="h-8 w-8 text-center !leading-[22px] font-[10px]">+</p>
|
||||
</a>
|
||||
</Flex>
|
||||
</Titlebar>
|
||||
{/snippet}
|
||||
|
||||
{#snippet McpServerView(server: IMcpServer)}
|
||||
<Deleteable ondelete={() => destroy(server)}>
|
||||
<Link
|
||||
href={`/mcp-servers/${server.name}`}
|
||||
class="w-full py-3 pl-8 text-sm hover:cursor-pointer"
|
||||
activeClass="text-purple border-l border-l-purple"
|
||||
>
|
||||
{server.name}
|
||||
</Link>
|
||||
</Deleteable>
|
||||
{/snippet}
|
||||
|
||||
{#snippet RegistryView(registry: Registry)}
|
||||
<Link
|
||||
href={`/mcp-servers/${registry.name.toLowerCase()}`}
|
||||
class="mb-4 flex h-full w-full items-center pl-8"
|
||||
activeClass="text-purple"
|
||||
>
|
||||
<Svg name={registry.icon} class="mr-4 h-6 w-6" />
|
||||
{registry.name}
|
||||
</Link>
|
||||
{/snippet}
|
||||
|
||||
<Layout {titlebar}>
|
||||
<Flex class="h-full items-start">
|
||||
<Flex class="border-r-light h-full w-[300px] flex-col items-start border-r">
|
||||
<List items={mcpServers} itemView={McpServerView} />
|
||||
<List
|
||||
items={registries}
|
||||
itemView={RegistryView}
|
||||
borderless
|
||||
title="Registries"
|
||||
class="mt-4"
|
||||
titleClass="pl-8 my-4"
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex class="h-full w-[calc(100%-300px)] items-start p-8">
|
||||
{@render children?.()}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Layout>
|
||||
@@ -1,68 +1,13 @@
|
||||
<script lang="ts">
|
||||
import Box from '$components/Box.svelte';
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Layout from '$components/Layouts/Default.svelte';
|
||||
import Svg from '$components/Svg.svelte';
|
||||
import Titlebar from '$components/Titlebar.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import McpServer, { type IMcpServer } from '$lib/models/mcp-server';
|
||||
|
||||
let mcpServers: IMcpServer[] = $derived(McpServer.all());
|
||||
const server: IMcpServer = McpServer.first();
|
||||
|
||||
function addServer() {
|
||||
mcpServers.push(McpServer.default());
|
||||
}
|
||||
|
||||
async function save(server: IMcpServer) {
|
||||
await McpServer.save({
|
||||
...server,
|
||||
metadata: server.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
async function destroy(server: IMcpServer) {
|
||||
if (!server.id) return;
|
||||
await McpServer.delete(server.id);
|
||||
if (server) {
|
||||
goto(`/mcp-servers/${server.name}`);
|
||||
} else {
|
||||
goto('/mcp-servers/smithery');
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet titlebar()}
|
||||
<Titlebar class="h-[60px] w-full">
|
||||
<Flex
|
||||
class=" border-r-light h-full w-[300px] items-center justify-between border-r px-8 pr-4"
|
||||
>
|
||||
<h1 class="font-[500]">MCP Servers</h1>
|
||||
<button
|
||||
class="border-light h-8 w-8 rounded-md border hover:cursor-pointer"
|
||||
onclick={() => addServer()}
|
||||
>
|
||||
<p class="h-8 w-8 text-center !leading-[22px] font-[10px]">+</p>
|
||||
</button>
|
||||
</Flex>
|
||||
</Titlebar>
|
||||
{/snippet}
|
||||
|
||||
<Layout {titlebar}>
|
||||
<Flex class="w-full flex-col gap-4 p-8">
|
||||
{#each mcpServers as server, i (i)}
|
||||
<Box class="m-0 w-full">
|
||||
<input
|
||||
bind:value={server.command}
|
||||
onchange={() => save(server)}
|
||||
type="text"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
spellcheck={false}
|
||||
placeholder="uvx | npx COMMAND [args]"
|
||||
class="focus:border-purple/50 text-light w-full rounded-md px-2 outline-none"
|
||||
/>
|
||||
|
||||
<button
|
||||
onclick={() => destroy(server)}
|
||||
class="text-dark hover:text-red ml-4 h-4 w-4 transition duration-300 hover:cursor-pointer"
|
||||
>
|
||||
<Svg name="Delete" />
|
||||
</button>
|
||||
</Box>
|
||||
{/each}
|
||||
</Flex>
|
||||
</Layout>
|
||||
|
||||
10
src/routes/mcp-servers/[name]/+page.svelte
Normal file
10
src/routes/mcp-servers/[name]/+page.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
|
||||
import McpServerView from '$components/McpServer.svelte';
|
||||
import McpServer, { type IMcpServer } from '$lib/models/mcp-server';
|
||||
|
||||
const server: IMcpServer = $derived(McpServer.findBy({ name: page.params.name }));
|
||||
</script>
|
||||
|
||||
<McpServerView {server} />
|
||||
8
src/routes/mcp-servers/new/+page.svelte
Normal file
8
src/routes/mcp-servers/new/+page.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import McpServerView from '$components/McpServer.svelte';
|
||||
import McpServer, { type IMcpServer } from '$lib/models/mcp-server';
|
||||
|
||||
const server: IMcpServer = $state(McpServer.default());
|
||||
</script>
|
||||
|
||||
<McpServerView {server} />
|
||||
89
src/routes/mcp-servers/smithery/+page.svelte
Normal file
89
src/routes/mcp-servers/smithery/+page.svelte
Normal file
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
import Button from '$components/Button.svelte';
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Input from '$components/Input.svelte';
|
||||
import Scrollable from '$components/Scrollable.svelte';
|
||||
import Card from '$components/Smithery/Card.svelte';
|
||||
import Configuration from '$components/Smithery/Configuration.svelte';
|
||||
import type { McpConfig } from '$lib/mcp';
|
||||
import McpServer from '$lib/models/mcp-server';
|
||||
import type { CompactServer, ConfigSchema, Server } from '$lib/smithery';
|
||||
import { Client } from '$lib/smithery/client';
|
||||
import { debounce } from '$lib/util.svelte';
|
||||
|
||||
const { data }: PageProps = $props();
|
||||
|
||||
let servers = $state(data.servers);
|
||||
let page: number = $state(1);
|
||||
let query: string = $state('');
|
||||
|
||||
let serverToInstall: Server | null = $state(null);
|
||||
|
||||
function closeConfigure() {
|
||||
serverToInstall = null;
|
||||
}
|
||||
|
||||
async function loadNextPage() {
|
||||
page += 1;
|
||||
servers = servers.concat(await new Client().servers(page));
|
||||
}
|
||||
|
||||
async function search() {
|
||||
const client = new Client();
|
||||
|
||||
if (query !== '') {
|
||||
servers = await client.search(query);
|
||||
} else {
|
||||
servers = await client.servers();
|
||||
}
|
||||
}
|
||||
|
||||
function configSchemaFor(server: Server): ConfigSchema {
|
||||
return server.connections.findBy('type', 'stdio')?.configSchema || {};
|
||||
}
|
||||
|
||||
async function configure(_server: CompactServer) {
|
||||
serverToInstall = await new Client().server(_server.qualifiedName);
|
||||
}
|
||||
|
||||
async function install(config: McpConfig) {
|
||||
serverToInstall = null;
|
||||
await McpServer.create(config);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if serverToInstall}
|
||||
<Configuration
|
||||
server={serverToInstall}
|
||||
config={configSchemaFor(serverToInstall)}
|
||||
onCancel={closeConfigure}
|
||||
onInstall={install}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Scrollable class="!h-content pr-2">
|
||||
<Flex class="w-full">
|
||||
<Input
|
||||
bind:value={query}
|
||||
class="mb-8"
|
||||
label={false}
|
||||
name="search"
|
||||
onkeyup={debounce(search)}
|
||||
placeholder="Search Smithery..."
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex class="grid w-full auto-cols-max auto-rows-max grid-cols-3 items-start gap-4">
|
||||
{#each servers as server (server.qualifiedName)}
|
||||
<Card {server} onInstall={configure} />
|
||||
{/each}
|
||||
</Flex>
|
||||
|
||||
<Flex class="w-full justify-center">
|
||||
<Button onclick={loadNextPage} class="border-light text-medium m-auto mt-8">
|
||||
Load More
|
||||
</Button>
|
||||
</Flex>
|
||||
</Scrollable>
|
||||
16
src/routes/mcp-servers/smithery/+page.ts
Normal file
16
src/routes/mcp-servers/smithery/+page.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
import type { CompactServer } from '$lib/smithery';
|
||||
import { Client } from '$lib/smithery/client';
|
||||
|
||||
interface Response {
|
||||
servers: CompactServer[];
|
||||
}
|
||||
|
||||
export const load: PageLoad = async (): Promise<Response> => {
|
||||
const client = new Client();
|
||||
|
||||
return {
|
||||
servers: await client.servers(),
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,12 @@ const config = {
|
||||
prerender: {
|
||||
'entries': [
|
||||
"/",
|
||||
"/error",
|
||||
"/chat/[session_id]",
|
||||
"/mcp-servers",
|
||||
"/mcp-servers/new",
|
||||
"/mcp-servers/[name]",
|
||||
"/mcp-servers/smithery",
|
||||
"/models",
|
||||
"/settings",
|
||||
],
|
||||
|
||||
@@ -3,4 +3,7 @@ import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
build: {
|
||||
sourcemap: true,
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user