From df5f4df76dc045dd71a0f137579308984947d722 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 9 Oct 2025 13:08:01 -0700 Subject: [PATCH 1/4] fix lto, add spider changes --- Cargo.lock | 99 +- .../dependencies/anthropic-api-key-manager | 2 +- hyperdrive/packages/file-explorer/Cargo.lock | 11 +- .../components/ContextMenu/ContextMenu.tsx | 6 +- .../components/FileExplorer/FileExplorer.tsx | 18 +- .../src/components/FileExplorer/FileItem.tsx | 24 +- .../src/components/FileExplorer/FileList.tsx | 14 +- .../components/ShareDialog/ShareDialog.tsx | 8 +- .../ui/src/components/Upload/UploadZone.tsx | 6 +- .../ui/src/store/fileExplorer.ts | 6 +- hyperdrive/packages/spider/.gitignore | 4 + hyperdrive/packages/spider/Cargo.lock | 180 +- hyperdrive/packages/spider/Cargo.toml | 1 + hyperdrive/packages/spider/pkg/manifest.json | 29 +- hyperdrive/packages/spider/spider/Cargo.toml | 6 +- hyperdrive/packages/spider/spider/src/lib.rs | 443 +- .../spider/spider/src/provider/anthropic.rs | 39 +- .../src/tool_providers/build_container.rs | 38 +- .../spider/src/tool_providers/hyperware.rs | 10 +- .../packages/spider/spider/src/types.rs | 151 +- .../packages/spider/ttstt-ui/index.html | 17 + .../spider/ttstt-ui/package-lock.json | 3674 +++++++++++++++++ .../packages/spider/ttstt-ui/package.json | 31 + .../packages/spider/ttstt-ui/src/App.tsx | 115 + .../ttstt-ui/src/components/ApiKeysPage.tsx | 184 + .../ttstt-ui/src/components/SettingsPage.tsx | 335 ++ .../ttstt-ui/src/components/TestPage.tsx | 105 + .../packages/spider/ttstt-ui/src/index.css | 368 ++ .../packages/spider/ttstt-ui/src/main.tsx | 12 + .../spider/ttstt-ui/src/store/ttstt.ts | 374 ++ .../ttstt-ui/src/types/caller-utils.d.ts | 5 + .../spider/ttstt-ui/src/types/global.ts | 32 + .../spider/ttstt-ui/src/types/ttstt.ts | 41 + .../packages/spider/ttstt-ui/src/utils/api.ts | 20 + .../spider/ttstt-ui/src/vite-env.d.ts | 1 + .../packages/spider/ttstt-ui/tsconfig.json | 26 + .../spider/ttstt-ui/tsconfig.node.json | 11 + .../packages/spider/ttstt-ui/vite.config.ts | 67 + hyperdrive/packages/spider/ttstt/Cargo.toml | 62 + hyperdrive/packages/spider/ttstt/src/icon | 1 + hyperdrive/packages/spider/ttstt/src/lib.rs | 1058 +++++ .../packages/spider/ui/src/auth/anthropic.ts | 4 +- .../spider/ui/src/components/Chat.tsx | 9 +- .../spider/ui/src/services/websocket.ts | 4 +- .../packages/spider/ui/src/store/spider.ts | 162 +- .../packages/spider/ui/src/types/websocket.ts | 5 +- .../packages/spider/ui/src/utils/api.ts | 72 +- scripts/build-packages/Cargo.toml | 2 +- scripts/build-packages/src/main.rs | 1 + 49 files changed, 7515 insertions(+), 378 deletions(-) create mode 100644 hyperdrive/packages/spider/ttstt-ui/index.html create mode 100644 hyperdrive/packages/spider/ttstt-ui/package-lock.json create mode 100644 hyperdrive/packages/spider/ttstt-ui/package.json create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/App.tsx create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/components/ApiKeysPage.tsx create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/components/SettingsPage.tsx create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/components/TestPage.tsx create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/index.css create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/main.tsx create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/store/ttstt.ts create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/types/caller-utils.d.ts create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/types/global.ts create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/types/ttstt.ts create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/utils/api.ts create mode 100644 hyperdrive/packages/spider/ttstt-ui/src/vite-env.d.ts create mode 100644 hyperdrive/packages/spider/ttstt-ui/tsconfig.json create mode 100644 hyperdrive/packages/spider/ttstt-ui/tsconfig.node.json create mode 100644 hyperdrive/packages/spider/ttstt-ui/vite.config.ts create mode 100644 hyperdrive/packages/spider/ttstt/Cargo.toml create mode 100644 hyperdrive/packages/spider/ttstt/src/icon create mode 100644 hyperdrive/packages/spider/ttstt/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2f78e1cef..a8038028c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3557,10 +3557,12 @@ dependencies = [ "argon2", "base64 0.22.1", "bincode", + "bytes", "chrono", "clap", "crossterm", "dashmap 5.5.3", + "dirs", "futures", "generic-array", "hex", @@ -3597,6 +3599,7 @@ dependencies = [ "warp", "wasmtime", "wasmtime-wasi", + "wasmtime-wasi-io", "web-push", "zip 1.1.4", ] @@ -3641,9 +3644,9 @@ dependencies = [ [[package]] name = "hyperprocess_macro" version = "0.1.0" -source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=ed99c19#ed99c19c1e4f511786b2c827fb909ba3d3950238" +source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=98fac21#98fac2177df21a39a0950d48b248e1b12013f7f0" dependencies = [ - "hyperware_process_lib 2.0.1", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=232fe25)", "proc-macro2", "quote", "syn 2.0.104", @@ -3652,9 +3655,9 @@ dependencies = [ [[package]] name = "hyperware-anthropic-sdk" version = "0.1.0" -source = "git+https://github.com/hyperware-ai/hyperware-anthropic-sdk?rev=363630c#363630c261c4cc8673b92a1f2e835bab8263c866" +source = "git+https://github.com/hyperware-ai/hyperware-anthropic-sdk?rev=607bbc6#607bbc6da2601b06f13729a846ebe2419e1dcf6c" dependencies = [ - "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=753dac3)", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=232fe25)", "rand 0.8.5", "serde", "serde_json", @@ -3663,33 +3666,13 @@ dependencies = [ ] [[package]] -name = "hyperware_process_lib" -version = "2.0.1" -source = "git+https://github.com/hyperware-ai/process_lib?rev=c27a881#c27a881e764920636ac025112533a714d622793c" +name = "hyperware-parse-wit" +version = "0.1.0" dependencies = [ - "alloy", - "alloy-primitives", - "alloy-sol-macro", - "alloy-sol-types", "anyhow", - "base64 0.22.1", - "bincode", - "color-eyre", - "futures-util", - "http 1.3.1", - "mime_guess", - "rand 0.8.5", - "regex", - "rmp-serde", - "serde", "serde_json", - "thiserror 1.0.69", - "tracing", - "tracing-error", - "tracing-subscriber", - "url", - "uuid 1.17.0", - "wit-bindgen 0.42.1", + "wit-parser 0.220.1", + "zip 2.4.2", ] [[package]] @@ -3726,7 +3709,7 @@ dependencies = [ [[package]] name = "hyperware_process_lib" version = "2.2.0" -source = "git+https://github.com/hyperware-ai/process_lib?rev=4beff93#4beff9389598978d2f97e4cd6d1d0f74a7acac0f" +source = "git+https://github.com/hyperware-ai/process_lib?rev=232fe25#232fe2526f383c88efc2ef3a289deba2e193d68f" dependencies = [ "alloy", "alloy-primitives", @@ -3756,7 +3739,7 @@ dependencies = [ [[package]] name = "hyperware_process_lib" version = "2.2.0" -source = "git+https://github.com/hyperware-ai/process_lib?rev=753dac3#753dac3d58cbf924264b47d7c0b845e2fcc03e32" +source = "git+https://github.com/hyperware-ai/process_lib?rev=4beff93#4beff9389598978d2f97e4cd6d1d0f74a7acac0f" dependencies = [ "alloy", "alloy-primitives", @@ -4311,7 +4294,7 @@ dependencies = [ [[package]] name = "kit" version = "3.1.1" -source = "git+https://github.com/hyperware-ai/kit?rev=aac33b6#aac33b6f25434e12bce8c28072d216a16c7abe52" +source = "git+https://github.com/hyperware-ai/kit?rev=275f02c#275f02c839e239083ba656871e10be845f4d85f5" dependencies = [ "alloy", "alloy-sol-macro", @@ -6625,9 +6608,10 @@ dependencies = [ "base64 0.21.7", "chrono", "http 1.3.1", - "hyperprocess_macro 0.1.0 (git+https://github.com/hyperware-ai/hyperprocess-macro?rev=ed99c19)", + "hyperprocess_macro 0.1.0 (git+https://github.com/hyperware-ai/hyperprocess-macro?rev=98fac21)", "hyperware-anthropic-sdk", - "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=753dac3)", + "hyperware-parse-wit", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=232fe25)", "process_macros", "rmp-serde", "serde", @@ -6637,6 +6621,7 @@ dependencies = [ "url", "uuid 1.17.0", "wit-bindgen 0.42.1", + "wit-parser 0.220.1", ] [[package]] @@ -6646,7 +6631,7 @@ dependencies = [ "anyhow", "futures", "futures-util", - "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=753dac3)", + "hyperware_process_lib 2.2.0 (git+https://github.com/hyperware-ai/process_lib?rev=232fe25)", "once_cell", "process_macros", "serde", @@ -7928,6 +7913,17 @@ dependencies = [ "wasmparser 0.230.0", ] +[[package]] +name = "wasmparser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25" +dependencies = [ + "bitflags 2.9.1", + "indexmap", + "semver 1.0.26", +] + [[package]] name = "wasmparser" version = "0.227.1" @@ -8954,6 +8950,24 @@ dependencies = [ "wit-parser 0.230.0", ] +[[package]] +name = "wit-parser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.26", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.220.1", +] + [[package]] name = "wit-parser" version = "0.227.1" @@ -9219,6 +9233,23 @@ dependencies = [ "zstd 0.13.3", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.12", + "zopfli", +] + [[package]] name = "zopfli" version = "0.8.2" diff --git a/hyperdrive/dependencies/anthropic-api-key-manager b/hyperdrive/dependencies/anthropic-api-key-manager index ccfed5955..aeaa740d0 160000 --- a/hyperdrive/dependencies/anthropic-api-key-manager +++ b/hyperdrive/dependencies/anthropic-api-key-manager @@ -1 +1 @@ -Subproject commit ccfed5955f2a42658aafc09a73f296fe28f3eb6b +Subproject commit aeaa740d03fbaa29de82f7983aa8c06137d57bfd diff --git a/hyperdrive/packages/file-explorer/Cargo.lock b/hyperdrive/packages/file-explorer/Cargo.lock index 5d29c4e71..16213b04f 100644 --- a/hyperdrive/packages/file-explorer/Cargo.lock +++ b/hyperdrive/packages/file-explorer/Cargo.lock @@ -1330,9 +1330,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide 0.8.9", @@ -2131,6 +2131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -3079,6 +3080,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" diff --git a/hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx b/hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx index c870ee11c..6564be004 100644 --- a/hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/hyperdrive/packages/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useRef, useState } from 'react'; -import { FileInfo, unshare_file, delete_file, delete_directory } from '../../lib/api'; +import { FileExplorer } from '../../lib/api'; import useFileExplorerStore from '../../store/fileExplorer'; import './ContextMenu.css'; interface ContextMenuProps { position: { x: number; y: number }; - file: FileInfo; + file: FileExplorer.FileInfo; onClose: () => void; onShare: () => void; onDelete: () => void; @@ -84,7 +84,7 @@ const ContextMenu: React.FC = ({ position, file, onClose, onSh const handleUnshare = async () => { try { - await unshare_file(file.path); + await FileExplorer.unshare_file(file.path); removeSharedLink(file.path); onClose(); } catch (err) { diff --git a/hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileExplorer.tsx b/hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileExplorer.tsx index 89dd45068..a317c9495 100644 --- a/hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileExplorer.tsx +++ b/hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileExplorer.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import useFileExplorerStore from '../../store/fileExplorer'; -import { list_directory, create_directory, create_file, delete_file, delete_directory, FileInfo, get_current_directory } from '../../lib/api'; +import { FileExplorer as FileExplorerAPI } from '../../lib/api'; import FileList from './FileList'; import Breadcrumb from './Breadcrumb'; import Toolbar from './Toolbar'; @@ -43,7 +43,7 @@ const FileExplorer: React.FC = () => { try { setLoading(true); setError(null); - const fileList = await list_directory(path); + const fileList = await FileExplorerAPI.list_directory(path); // Backend returns files for the requested directory with 2 levels of depth // We need to include all files so the tree structure works, but we'll filter @@ -64,9 +64,9 @@ const FileExplorer: React.FC = () => { } }; - const loadSubdirectory = async (path: string): Promise => { + const loadSubdirectory = async (path: string): Promise => { try { - const fileList = await list_directory(path); + const fileList = await FileExplorerAPI.list_directory(path); // Filter out the directory itself and return only its contents const filteredFiles = fileList.filter(file => { @@ -85,7 +85,7 @@ const FileExplorer: React.FC = () => { useEffect(() => { const initializeDirectory = async () => { try { - const cwd = await get_current_directory(); + const cwd = await FileExplorerAPI.get_current_directory(); setCurrentPath(cwd); } catch (err) { // If getting cwd fails, fall back to root @@ -119,7 +119,7 @@ const FileExplorer: React.FC = () => { : `${currentPath}/${folderName}`; try { - await create_directory(newPath); + await FileExplorerAPI.create_directory(newPath); await loadDirectory(currentPath); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create folder'); @@ -137,7 +137,7 @@ const FileExplorer: React.FC = () => { try { // Create an empty file - await create_file(newPath, []); + await FileExplorerAPI.create_file(newPath, []); await loadDirectory(currentPath); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create file'); @@ -153,9 +153,9 @@ const FileExplorer: React.FC = () => { for (const path of selectedFiles) { const file = files.find(f => f.path === path); if (file?.is_directory) { - await delete_directory(path); + await FileExplorerAPI.delete_directory(path); } else { - await delete_file(path); + await FileExplorerAPI.delete_file(path); } } clearSelection(); diff --git a/hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileItem.tsx b/hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileItem.tsx index 0664485c6..f97bbfdd4 100644 --- a/hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileItem.tsx +++ b/hyperdrive/packages/file-explorer/ui/src/components/FileExplorer/FileItem.tsx @@ -1,16 +1,16 @@ import React, { useState, useRef, useEffect } from 'react'; -import { FileInfo, delete_file, delete_directory } from '../../lib/api'; +import { FileExplorer } from '../../lib/api'; import useFileExplorerStore from '../../store/fileExplorer'; import ContextMenu from '../ContextMenu/ContextMenu'; import ShareDialog from '../ShareDialog/ShareDialog'; import './FileItem.css'; interface FileItemProps { - file: FileInfo & { children?: FileInfo[] }; + file: FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] }; viewMode: 'list' | 'grid'; onNavigate: (path: string) => void; depth?: number; - onLoadSubdirectory?: (path: string) => Promise; + onLoadSubdirectory?: (path: string) => Promise; onDelete?: () => void; } @@ -26,7 +26,7 @@ const FileItem: React.FC = ({ file, viewMode, onNavigate, depth = const [isExpanded, setIsExpanded] = useState(false); const [childrenLoaded, setChildrenLoaded] = useState(false); - const [loadedChildren, setLoadedChildren] = useState<(FileInfo & { children?: FileInfo[] })[]>([]); + const [loadedChildren, setLoadedChildren] = useState<(FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] })[]>([]); // Touch handling for iOS long-press const touchTimerRef = useRef(null); @@ -43,9 +43,9 @@ const FileItem: React.FC = ({ file, viewMode, onNavigate, depth = // Remove file selection on single click - no action for files }; - const buildTreeFromFlatList = (flatList: FileInfo[], parentPath: string): (FileInfo & { children?: FileInfo[] })[] => { - const fileMap = new Map(); - const topLevelFiles: (FileInfo & { children?: FileInfo[] })[] = []; + const buildTreeFromFlatList = (flatList: FileExplorer.FileInfo[], parentPath: string): (FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] })[] => { + const fileMap = new Map(); + const topLevelFiles: (FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] })[] = []; // First pass: create map of all files flatList.forEach(file => { @@ -69,7 +69,7 @@ const FileItem: React.FC = ({ file, viewMode, onNavigate, depth = }); // Sort files: directories first, then by name - const sortFiles = (files: (FileInfo & { children?: FileInfo[] })[]) => { + const sortFiles = (files: (FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] })[]) => { return [...files].sort((a, b) => { if (a.is_directory && !b.is_directory) return -1; if (!a.is_directory && b.is_directory) return 1; @@ -78,7 +78,7 @@ const FileItem: React.FC = ({ file, viewMode, onNavigate, depth = }; // Recursively sort all children - const sortRecursive = (files: (FileInfo & { children?: FileInfo[] })[]) => { + const sortRecursive = (files: (FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] })[]) => { const sorted = sortFiles(files); sorted.forEach(file => { if (file.children && file.children.length > 0) { @@ -184,9 +184,9 @@ const FileItem: React.FC = ({ file, viewMode, onNavigate, depth = try { if (file.is_directory) { - await delete_directory(file.path); + await FileExplorer.delete_directory(file.path); } else { - await delete_file(file.path); + await FileExplorer.delete_file(file.path); } // Call the parent's onDelete callback to refresh the list if (onDelete) { @@ -273,7 +273,7 @@ const FileItem: React.FC = ({ file, viewMode, onNavigate, depth = {childrenToRender.map((child) => ( void; currentPath: string; - onLoadSubdirectory?: (path: string) => Promise; + onLoadSubdirectory?: (path: string) => Promise; onDelete?: () => void; } @@ -23,8 +23,8 @@ const FileList: React.FC = ({ files, viewMode, loading, onNavigat } // Build tree structure from flat list - const fileMap = new Map(); - const topLevelFiles: (FileInfo & { children?: FileInfo[] })[] = []; + const fileMap = new Map(); + const topLevelFiles: (FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] })[] = []; // First pass: create map of all files files.forEach(file => { @@ -63,7 +63,7 @@ const FileList: React.FC = ({ files, viewMode, loading, onNavigat }); // Sort files: directories first, then by name - const sortFiles = (files: (FileInfo & { children?: FileInfo[] })[]) => { + const sortFiles = (files: (FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] })[]) => { return [...files].sort((a, b) => { if (a.is_directory && !b.is_directory) return -1; if (!a.is_directory && b.is_directory) return 1; @@ -72,7 +72,7 @@ const FileList: React.FC = ({ files, viewMode, loading, onNavigat }; // Recursively sort all children - const sortRecursive = (files: (FileInfo & { children?: FileInfo[] })[]) => { + const sortRecursive = (files: (FileExplorer.FileInfo & { children?: FileExplorer.FileInfo[] })[]) => { const sorted = sortFiles(files); sorted.forEach(file => { if (file.children && file.children.length > 0) { diff --git a/hyperdrive/packages/file-explorer/ui/src/components/ShareDialog/ShareDialog.tsx b/hyperdrive/packages/file-explorer/ui/src/components/ShareDialog/ShareDialog.tsx index 40379aa66..d88efb869 100644 --- a/hyperdrive/packages/file-explorer/ui/src/components/ShareDialog/ShareDialog.tsx +++ b/hyperdrive/packages/file-explorer/ui/src/components/ShareDialog/ShareDialog.tsx @@ -1,16 +1,16 @@ import React, { useState, useEffect, useRef } from 'react'; -import { FileInfo, AuthScheme, share_file } from '../../lib/api'; +import { FileExplorer } from '../../lib/api'; import useFileExplorerStore from '../../store/fileExplorer'; import QRCode from 'qrcode'; import './ShareDialog.css'; interface ShareDialogProps { - file: FileInfo; + file: FileExplorer.FileInfo; onClose: () => void; } const ShareDialog: React.FC = ({ file, onClose }) => { - const [authScheme, setAuthScheme] = useState("Public"); + const [authScheme, setAuthScheme] = useState(FileExplorer.AuthScheme.Public); const [shareLink, setShareLink] = useState(''); const [loading, setLoading] = useState(false); const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); @@ -70,7 +70,7 @@ const ShareDialog: React.FC = ({ file, onClose }) => { const handleShare = async () => { setLoading(true); try { - const link = await share_file(file.path, authScheme); + const link = await FileExplorer.share_file(file.path, authScheme); // Remove first element of origin (e.g., http://foo.bar.com -> http://bar.com) let origin = window.location.origin; diff --git a/hyperdrive/packages/file-explorer/ui/src/components/Upload/UploadZone.tsx b/hyperdrive/packages/file-explorer/ui/src/components/Upload/UploadZone.tsx index 4c0a399f0..921ca79bc 100644 --- a/hyperdrive/packages/file-explorer/ui/src/components/Upload/UploadZone.tsx +++ b/hyperdrive/packages/file-explorer/ui/src/components/Upload/UploadZone.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef } from 'react'; -import { upload_file, create_directory } from '../../lib/api'; +import { FileExplorer } from '../../lib/api'; import useFileExplorerStore from '../../store/fileExplorer'; import './UploadZone.css'; @@ -143,7 +143,7 @@ const UploadZone: React.FC = ({ currentPath, onUploadComplete, for (const folderName of pathParts) { uploadPath = uploadPath === '/' ? `/${folderName}` : `${uploadPath}/${folderName}`; try { - await create_directory(uploadPath); + await FileExplorer.create_directory(uploadPath); } catch (err) { // Ignore if directory already exists console.log(`Directory ${uploadPath} might already exist`); @@ -154,7 +154,7 @@ const UploadZone: React.FC = ({ currentPath, onUploadComplete, // Simulate upload progress updateUploadProgress(fileId, 50); - await upload_file(uploadPath, fileName, contentArray); + await FileExplorer.upload_file(uploadPath, fileName, contentArray); updateUploadProgress(fileId, 100); onUploadComplete(); diff --git a/hyperdrive/packages/file-explorer/ui/src/store/fileExplorer.ts b/hyperdrive/packages/file-explorer/ui/src/store/fileExplorer.ts index 9e04b5854..140700fc1 100644 --- a/hyperdrive/packages/file-explorer/ui/src/store/fileExplorer.ts +++ b/hyperdrive/packages/file-explorer/ui/src/store/fileExplorer.ts @@ -1,9 +1,9 @@ import { create } from 'zustand'; -import { FileInfo } from '../lib/api'; +import { FileExplorer } from '../lib/api'; interface FileExplorerStore { currentPath: string; - files: FileInfo[]; + files: FileExplorer.FileInfo[]; selectedFiles: string[]; expandedDirectories: Set; uploadProgress: Map; @@ -13,7 +13,7 @@ interface FileExplorerStore { // Actions setCurrentPath: (path: string) => void; - setFiles: (files: FileInfo[]) => void; + setFiles: (files: FileExplorer.FileInfo[]) => void; selectFile: (path: string) => void; deselectFile: (path: string) => void; toggleFileSelection: (path: string) => void; diff --git a/hyperdrive/packages/spider/.gitignore b/hyperdrive/packages/spider/.gitignore index 6b6711d68..0307ff5e3 100644 --- a/hyperdrive/packages/spider/.gitignore +++ b/hyperdrive/packages/spider/.gitignore @@ -12,11 +12,15 @@ /pkg/*zip /pkg/*wasm /pkg/ui +/pkg/ttstt-ui # UI dependencies and build /ui/node_modules/ /ui/dist/ /ui/.vite/ +/ttstt-ui/node_modules/ +/ttstt-ui/dist/ +/ttstt-ui/.vite/ *.log # IDE files diff --git a/hyperdrive/packages/spider/Cargo.lock b/hyperdrive/packages/spider/Cargo.lock index a7533668e..a2f073349 100644 --- a/hyperdrive/packages/spider/Cargo.lock +++ b/hyperdrive/packages/spider/Cargo.lock @@ -592,6 +592,15 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -1130,6 +1139,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -1685,9 +1705,9 @@ dependencies = [ [[package]] name = "hyperprocess_macro" version = "0.1.0" -source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=ed99c19#ed99c19c1e4f511786b2c827fb909ba3d3950238" +source = "git+https://github.com/hyperware-ai/hyperprocess-macro?rev=98fac21#98fac2177df21a39a0950d48b248e1b12013f7f0" dependencies = [ - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "proc-macro2", "quote", "syn 2.0.106", @@ -1696,9 +1716,9 @@ dependencies = [ [[package]] name = "hyperware-anthropic-sdk" version = "0.1.0" -source = "git+https://github.com/hyperware-ai/hyperware-anthropic-sdk?rev=363630c#363630c261c4cc8673b92a1f2e835bab8263c866" +source = "git+https://github.com/hyperware-ai/hyperware-anthropic-sdk?rev=607bbc6#607bbc6da2601b06f13729a846ebe2419e1dcf6c" dependencies = [ - "hyperware_process_lib 2.2.0", + "hyperware_process_lib", "rand 0.8.5", "serde", "serde_json", @@ -1707,39 +1727,60 @@ dependencies = [ ] [[package]] -name = "hyperware_process_lib" -version = "2.0.1" -source = "git+https://github.com/hyperware-ai/process_lib?rev=c27a881#c27a881e764920636ac025112533a714d622793c" +name = "hyperware-elevenlabs-tts" +version = "0.1.0" +source = "git+https://github.com/hyperware-ai/hyperware-elevenlabs-tts?rev=65899de#65899dea2b081ff982404c687040dd0bc92ed917" +dependencies = [ + "http", + "hyperware_process_lib", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "hyperware-openai-stt" +version = "0.1.0" +source = "git+https://github.com/hyperware-ai/hyperware-openai-ttstt?rev=0e6bf83#0e6bf834c39ebe3ab6624829848b50c83f18201f" dependencies = [ - "alloy", - "alloy-primitives", - "alloy-sol-macro", - "alloy-sol-types", - "anyhow", "base64 0.22.1", - "bincode", - "color-eyre", - "futures-util", "http", - "mime_guess", + "hyperware_process_lib", "rand 0.8.5", - "regex", - "rmp-serde", "serde", "serde_json", "thiserror 1.0.69", - "tracing", - "tracing-error", - "tracing-subscriber", "url", - "uuid", - "wit-bindgen 0.42.1", +] + +[[package]] +name = "hyperware-openai-tts" +version = "0.1.0" +source = "git+https://github.com/hyperware-ai/hyperware-openai-ttstt?rev=0e6bf83#0e6bf834c39ebe3ab6624829848b50c83f18201f" +dependencies = [ + "http", + "hyperware_process_lib", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "hyperware-parse-wit" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde_json", + "wit-parser 0.220.1", + "zip", ] [[package]] name = "hyperware_process_lib" version = "2.2.0" -source = "git+https://github.com/hyperware-ai/process_lib?rev=753dac3#753dac3d58cbf924264b47d7c0b845e2fcc03e32" +source = "git+https://github.com/hyperware-ai/process_lib?rev=232fe25#232fe2526f383c88efc2ef3a289deba2e193d68f" dependencies = [ "alloy", "alloy-primitives", @@ -3090,6 +3131,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.11" @@ -3134,7 +3181,8 @@ dependencies = [ "http", "hyperprocess_macro", "hyperware-anthropic-sdk", - "hyperware_process_lib 2.2.0", + "hyperware-parse-wit", + "hyperware_process_lib", "process_macros", "rmp-serde", "serde", @@ -3144,6 +3192,7 @@ dependencies = [ "url", "uuid", "wit-bindgen 0.42.1", + "wit-parser 0.220.1", ] [[package]] @@ -3153,7 +3202,7 @@ dependencies = [ "anyhow", "futures", "futures-util", - "hyperware_process_lib 2.2.0", + "hyperware_process_lib", "once_cell", "process_macros", "serde", @@ -3583,6 +3632,27 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttstt" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "chrono", + "hyperprocess_macro", + "hyperware-elevenlabs-tts", + "hyperware-openai-stt", + "hyperware-openai-tts", + "hyperware_process_lib", + "process_macros", + "serde", + "serde_json", + "spider_caller_utils", + "uuid", + "wit-bindgen 0.42.1", +] + [[package]] name = "typenum" version = "1.18.0" @@ -3833,6 +3903,17 @@ dependencies = [ "wasmparser 0.230.0", ] +[[package]] +name = "wasmparser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25" +dependencies = [ + "bitflags", + "indexmap", + "semver 1.0.26", +] + [[package]] name = "wasmparser" version = "0.227.1" @@ -4291,6 +4372,24 @@ dependencies = [ "wit-parser 0.230.0", ] +[[package]] +name = "wit-parser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.26", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.220.1", +] + [[package]] name = "wit-parser" version = "0.227.1" @@ -4459,3 +4558,32 @@ dependencies = [ "quote", "syn 2.0.106", ] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.15", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/hyperdrive/packages/spider/Cargo.toml b/hyperdrive/packages/spider/Cargo.toml index df6607d3c..1b21e4954 100644 --- a/hyperdrive/packages/spider/Cargo.toml +++ b/hyperdrive/packages/spider/Cargo.toml @@ -6,5 +6,6 @@ panic = "abort" [workspace] members = [ "spider", + "ttstt", ] resolver = "2" diff --git a/hyperdrive/packages/spider/pkg/manifest.json b/hyperdrive/packages/spider/pkg/manifest.json index 3fb3d4fa8..a7b19229f 100644 --- a/hyperdrive/packages/spider/pkg/manifest.json +++ b/hyperdrive/packages/spider/pkg/manifest.json @@ -5,23 +5,48 @@ "on_exit": "Restart", "request_networking": true, "request_capabilities": [ - "main:app-store:sys", + "chat:chat:ware.hypr", "homepage:homepage:sys", "http-client:distro:sys", "http-server:distro:sys", "hypermap-cacher:hypermap-cacher:sys", + "main:app-store:sys", "operator:hypergrid:ware.hypr", "timer:distro:sys", + "ttstt:spider:sys", "vfs:distro:sys" ], "grant_capabilities": [ - "main:app-store:sys", + "chat:chat:ware.hypr", "homepage:homepage:sys", "http-client:distro:sys", "http-server:distro:sys", "hypermap-cacher:hypermap-cacher:sys", + "main:app-store:sys", "operator:hypergrid:ware.hypr", "timer:distro:sys", + "ttstt:spider:sys", + "vfs:distro:sys" + ], + "public": false + }, + { + "process_name": "ttstt", + "process_wasm_path": "/ttstt.wasm", + "on_exit": "Restart", + "request_networking": false, + "request_capabilities": [ + "homepage:homepage:sys", + "http-client:distro:sys", + "http-server:distro:sys", + "spider:spider:sys", + "vfs:distro:sys" + ], + "grant_capabilities": [ + "homepage:homepage:sys", + "http-client:distro:sys", + "http-server:distro:sys", + "spider:spider:sys", "vfs:distro:sys" ], "public": false diff --git a/hyperdrive/packages/spider/spider/Cargo.toml b/hyperdrive/packages/spider/spider/Cargo.toml index 5a6107154..16fb2dc27 100644 --- a/hyperdrive/packages/spider/spider/Cargo.toml +++ b/hyperdrive/packages/spider/spider/Cargo.toml @@ -15,11 +15,11 @@ version = "0.4" [dependencies.hyperprocess_macro] git = "https://github.com/hyperware-ai/hyperprocess-macro" -rev = "66884c0" +rev = "98fac21" [dependencies.hyperware-anthropic-sdk] git = "https://github.com/hyperware-ai/hyperware-anthropic-sdk" -rev = "c0cbd5e" +rev = "607bbc6" [dependencies.hyperware-parse-wit] path = "../crates/hyperware-parse-wit" @@ -27,7 +27,7 @@ path = "../crates/hyperware-parse-wit" [dependencies.hyperware_process_lib] features = ["hyperapp"] git = "https://github.com/hyperware-ai/process_lib" -rev = "4beff93" +rev = "232fe25" [dependencies.serde] features = ["derive"] diff --git a/hyperdrive/packages/spider/spider/src/lib.rs b/hyperdrive/packages/spider/spider/src/lib.rs index fe984a202..69d2e8b55 100644 --- a/hyperdrive/packages/spider/spider/src/lib.rs +++ b/hyperdrive/packages/spider/spider/src/lib.rs @@ -3,17 +3,17 @@ use std::sync::Arc; use std::time::Instant; use chrono::Utc; -use serde_json::Value; +use serde_json::{json, Value}; use uuid::Uuid; -use hyperprocess_macro::*; +use hyperprocess_macro::hyperprocess; use hyperware_process_lib::{ homepage::add_to_homepage, http::{ client::{open_ws_connection, send_ws_client_push}, server::{send_ws_push, WsMessageType}, }, - hyperapp::source, + hyperapp::{add_response_header, source}, our, println, Address, LazyLoadBlob, ProcessId, Request, }; #[cfg(not(feature = "simulation-mode"))] @@ -24,18 +24,17 @@ use provider::create_llm_provider; mod types; use types::{ - AddMcpServerRequest, ApiKey, ApiKeyInfo, ChatClient, ChatRequest, ChatResponse, ConfigResponse, - ConnectMcpServerRequest, Conversation, ConversationMetadata, CreateSpiderKeyRequest, - DisconnectMcpServerRequest, ErrorResponse, GetConfigRequest, GetConversationRequest, - HypergridConnection, HypergridMessage, HypergridMessageType, JsonRpcNotification, - JsonRpcRequest, ListApiKeysRequest, ListConversationsRequest, ListMcpServersRequest, - ListSpiderKeysRequest, McpCapabilities, McpClientInfo, McpInitializeParams, McpRequestType, - McpServer, McpServerDetails, McpToolCallParams, McpToolInfo, Message, OAuthCodeExchangeRequest, - OAuthExchangeRequest, OAuthRefreshRequest, OAuthRefreshTokenRequest, OAuthTokenResponse, - PendingMcpRequest, ProcessRequest, ProcessResponse, RemoveApiKeyRequest, - RemoveMcpServerRequest, RevokeSpiderKeyRequest, SetApiKeyRequest, SpiderApiKey, SpiderState, - Tool, ToolCall, ToolExecutionResult, ToolResponseContent, ToolResponseContentItem, ToolResult, - TrialNotification, UpdateConfigRequest, WsClientMessage, WsConnection, WsServerMessage, + AddMcpServerReq, ApiKey, ApiKeyInfo, ChatClient, ChatReq, ChatRes, ConfigRes, + ConnectMcpServerReq, Conversation, ConversationMetadata, CreateSpiderKeyReq, + DisconnectMcpServerReq, ErrorRes, GetConfigReq, GetConversationReq, HypergridConnection, + HypergridMessage, HypergridMessageType, JsonRpcNotification, JsonRpcReq, ListApiKeysReq, + ListConversationsReq, ListMcpServersReq, ListSpiderKeysReq, McpCapabilities, McpClientInfo, + McpInitializeParams, McpRequestType, McpServer, McpServerDetails, McpToolCallParams, + McpToolInfo, Message, MessageContent, OAuthCodeExchangeReq, OAuthExchangeReq, OAuthRefreshReq, + OAuthRefreshTokenReq, OAuthTokenRes, PendingMcpReq, ProcessReq, ProcessRes, RemoveApiKeyReq, + RemoveMcpServerReq, RevokeSpiderKeyReq, SetApiKeyReq, SpiderApiKey, SpiderState, Tool, + ToolCall, ToolExecutionResult, ToolResponseContent, ToolResponseContentItem, ToolResult, + TrialNotification, UpdateConfigReq, WsClientMessage, WsConnection, WsServerMessage, }; mod utils; @@ -65,6 +64,7 @@ const API_KEY_DISPENSER_PROCESS_ID: (&str, &str, &str) = ( "ware.hypr", ); const HYPERGRID: &str = "operator:hypergrid:ware.hypr"; +const TTSTT: (&str, &str, &str) = ("ttstt", "spider", "sys"); #[hyperprocess( name = "Spider", @@ -443,7 +443,7 @@ impl SpiderState { String::new() }); - let connect_request = ConnectMcpServerRequest { + let connect_request = ConnectMcpServerReq { server_id: server_id.clone(), auth_key: admin_key, }; @@ -607,8 +607,8 @@ impl SpiderState { return; } - // Convert WsChatPayload to ChatRequest - let chat_request = ChatRequest { + // Convert WsChatPayload to ChatReq + let chat_request = ChatReq { api_key: client.api_key, messages: payload.messages, llm_provider: payload.llm_provider, @@ -792,7 +792,7 @@ impl SpiderState { } #[http] - async fn set_api_key(&mut self, request: SetApiKeyRequest) -> Result { + async fn set_api_key(&mut self, request: SetApiKeyReq) -> Result { // Validate write permission if !self.validate_permission(&request.auth_key, "write") { return Err("Unauthorized: API key lacks write permission".to_string()); @@ -814,7 +814,7 @@ impl SpiderState { } #[http] - async fn list_api_keys(&self, request: ListApiKeysRequest) -> Result, String> { + async fn list_api_keys(&self, request: ListApiKeysReq) -> Result, String> { // Validate read permission if !self.validate_permission(&request.auth_key, "read") { return Err("Unauthorized: API key lacks read permission".to_string()); @@ -835,7 +835,7 @@ impl SpiderState { } #[http] - async fn remove_api_key(&mut self, request: RemoveApiKeyRequest) -> Result { + async fn remove_api_key(&mut self, request: RemoveApiKeyReq) -> Result { // Validate write permission if !self.validate_permission(&request.auth_key, "write") { return Err("Unauthorized: API key lacks write permission".to_string()); @@ -855,7 +855,7 @@ impl SpiderState { #[http] async fn create_spider_key( &mut self, - request: CreateSpiderKeyRequest, + request: CreateSpiderKeyReq, ) -> Result { // Validate admin key let hypergrid: ProcessId = HYPERGRID.parse().unwrap(); @@ -880,7 +880,7 @@ impl SpiderState { #[http] async fn list_spider_keys( &self, - request: ListSpiderKeysRequest, + request: ListSpiderKeysReq, ) -> Result, String> { // Validate admin key if !self.validate_admin_key(&request.admin_key) { @@ -891,10 +891,7 @@ impl SpiderState { } #[http] - async fn revoke_spider_key( - &mut self, - request: RevokeSpiderKeyRequest, - ) -> Result { + async fn revoke_spider_key(&mut self, request: RevokeSpiderKeyReq) -> Result { // Validate admin key if !self.validate_admin_key(&request.admin_key) { return Err("Unauthorized: Invalid or non-admin Spider API key".to_string()); @@ -911,7 +908,7 @@ impl SpiderState { } #[http] - async fn add_mcp_server(&mut self, request: AddMcpServerRequest) -> Result { + async fn add_mcp_server(&mut self, request: AddMcpServerReq) -> Result { // Validate write permission if !self.validate_permission(&request.auth_key, "write") { return Err("Unauthorized: API key lacks write permission".to_string()); @@ -933,10 +930,7 @@ impl SpiderState { #[local] #[http] - async fn list_mcp_servers( - &self, - request: ListMcpServersRequest, - ) -> Result, String> { + async fn list_mcp_servers(&self, request: ListMcpServersReq) -> Result, String> { // Validate read permission if !self.validate_permission(&request.auth_key, "read") { return Err("Unauthorized: API key lacks read permission".to_string()); @@ -948,7 +942,7 @@ impl SpiderState { #[http] async fn disconnect_mcp_server( &mut self, - request: DisconnectMcpServerRequest, + request: DisconnectMcpServerReq, ) -> Result { // Validate write permission if !self.validate_permission(&request.auth_key, "write") { @@ -989,17 +983,14 @@ impl SpiderState { } #[http] - async fn remove_mcp_server( - &mut self, - request: RemoveMcpServerRequest, - ) -> Result { + async fn remove_mcp_server(&mut self, request: RemoveMcpServerReq) -> Result { // Validate write permission if !self.validate_permission(&request.auth_key, "write") { return Err("Unauthorized: API key lacks write permission".to_string()); } // First disconnect if connected - let disconnect_request = DisconnectMcpServerRequest { + let disconnect_request = DisconnectMcpServerReq { server_id: request.server_id.clone(), auth_key: request.auth_key.clone(), }; @@ -1017,10 +1008,7 @@ impl SpiderState { } #[http] - async fn connect_mcp_server( - &mut self, - request: ConnectMcpServerRequest, - ) -> Result { + async fn connect_mcp_server(&mut self, request: ConnectMcpServerReq) -> Result { // Validate write permission if !self.validate_permission(&request.auth_key, "write") { return Err("Unauthorized: API key lacks write permission".to_string()); @@ -1066,7 +1054,7 @@ impl SpiderState { ); // Send initialize request - let init_request = JsonRpcRequest { + let init_request = JsonRpcReq { jsonrpc: "2.0".to_string(), method: "initialize".to_string(), params: Some( @@ -1086,7 +1074,7 @@ impl SpiderState { // Store pending request self.pending_mcp_requests.insert( format!("init_{}", channel_id), - PendingMcpRequest { + PendingMcpReq { request_id: format!("init_{}", channel_id), conversation_id: None, server_id: request.server_id.clone(), @@ -1206,7 +1194,7 @@ impl SpiderState { #[http] async fn list_conversations( &self, - request: ListConversationsRequest, + request: ListConversationsReq, ) -> Result, String> { // Validate read permission if !self.validate_permission(&request.auth_key, "read") { @@ -1231,10 +1219,7 @@ impl SpiderState { } #[http] - async fn get_conversation( - &self, - request: GetConversationRequest, - ) -> Result { + async fn get_conversation(&self, request: GetConversationReq) -> Result { // Validate read permission if !self.validate_permission(&request.auth_key, "read") { return Err("Unauthorized: API key lacks read permission".to_string()); @@ -1252,13 +1237,13 @@ impl SpiderState { } #[http] - async fn get_config(&self, request: GetConfigRequest) -> Result { + async fn get_config(&self, request: GetConfigReq) -> Result { // Validate read permission if !self.validate_permission(&request.auth_key, "read") { return Err("Unauthorized: API key lacks read permission".to_string()); } - Ok(ConfigResponse { + Ok(ConfigRes { default_llm_provider: self.default_llm_provider.clone(), max_tokens: self.max_tokens, temperature: self.temperature, @@ -1268,7 +1253,7 @@ impl SpiderState { } #[http] - async fn update_config(&mut self, request: UpdateConfigRequest) -> Result { + async fn update_config(&mut self, request: UpdateConfigReq) -> Result { // Validate write permission if !self.validate_permission(&request.auth_key, "write") { return Err("Unauthorized: API key lacks write permission".to_string()); @@ -1368,9 +1353,17 @@ impl SpiderState { #[local] #[http] - async fn chat(&mut self, request: ChatRequest) -> Result { + async fn chat(&mut self, request: ChatReq) -> Result { // Use the shared internal chat processing logic (without WebSocket streaming) - self.process_chat_internal(request, None).await + let source = source(); + if source.publisher() == "sys" + && source.package() == "distro" + && source.process() == "http-server" + { + add_response_header("Content-Type".to_string(), "application/json".to_string()); + } + let result = self.process_chat_internal(request, None).await; + result } #[local] @@ -1379,23 +1372,20 @@ impl SpiderState { } #[local] - async fn process_request( - &mut self, - request: ProcessRequest, - ) -> Result { + async fn process_request(&mut self, request: ProcessReq) -> Result { match request.action.as_str() { "chat" => { - let chat_request: ChatRequest = serde_json::from_str(&request.payload) + let chat_request: ChatReq = serde_json::from_str(&request.payload) .map_err(|e| format!("Invalid chat request: {}", e))?; let result = self.chat(chat_request).await?; let serialized = serde_json::to_string(&result) .map_err(|e| format!("Failed to serialize chat response: {}", e))?; - Ok(ProcessResponse { + Ok(ProcessRes { success: true, data: serialized, }) } - _ => Ok(ProcessResponse { + _ => Ok(ProcessRes { success: false, data: format!("Unknown action: {}", request.action), }), @@ -1404,10 +1394,7 @@ impl SpiderState { // OAuth endpoints - proxy requests to Anthropic to avoid CORS #[http] - async fn exchange_oauth_token( - &self, - req: OAuthExchangeRequest, - ) -> Result { + async fn exchange_oauth_token(&self, req: OAuthExchangeReq) -> Result { use hyperware_process_lib::http::client::send_request_await_response; use hyperware_process_lib::http::Method; @@ -1417,7 +1404,7 @@ impl SpiderState { let state = parts.get(1).unwrap_or(&"").to_string(); // Prepare the request body - let body = OAuthCodeExchangeRequest { + let body = OAuthCodeExchangeReq { code, state, grant_type: "authorization_code".to_string(), @@ -1445,7 +1432,7 @@ impl SpiderState { if response.status().is_success() { // Parse the response body match serde_json::from_slice::(response.body()) { - Ok(json) => Ok(OAuthTokenResponse { + Ok(json) => Ok(OAuthTokenRes { refresh: json["refresh_token"].as_str().unwrap_or("").to_string(), access: json["access_token"].as_str().unwrap_or("").to_string(), expires: chrono::Utc::now().timestamp() as u64 @@ -1464,15 +1451,12 @@ impl SpiderState { } #[http] - async fn refresh_oauth_token( - &self, - req: OAuthRefreshRequest, - ) -> Result { + async fn refresh_oauth_token(&self, req: OAuthRefreshReq) -> Result { use hyperware_process_lib::http::client::send_request_await_response; use hyperware_process_lib::http::Method; // Prepare the request body - let body = OAuthRefreshTokenRequest { + let body = OAuthRefreshTokenReq { grant_type: "refresh_token".to_string(), refresh_token: req.refresh_token, client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e".to_string(), @@ -1497,7 +1481,7 @@ impl SpiderState { if response.status().is_success() { // Parse the response body match serde_json::from_slice::(response.body()) { - Ok(json) => Ok(OAuthTokenResponse { + Ok(json) => Ok(OAuthTokenRes { refresh: json["refresh_token"].as_str().unwrap_or("").to_string(), access: json["access_token"].as_str().unwrap_or("").to_string(), expires: chrono::Utc::now().timestamp() as u64 @@ -1602,12 +1586,129 @@ impl SpiderState { } } + // Helper function to convert text to audio using ttstt + async fn convert_text_to_audio(&self, text: String) -> Result { + // Create TTS request for ttstt + #[derive(serde::Serialize)] + struct TtsRequest { + text: String, + provider: Option, + model: Option, + voice: Option, + api_key: Option, + } + + let tts_request = TtsRequest { + text, + provider: None, + model: None, + voice: None, + api_key: None, // ttstt will accept requests from spider without auth + }; + + // Send request to ttstt using the tts endpoint + let request = Request::to(("our", TTSTT)) + .body( + serde_json::to_vec(&json!({ + "Tts": tts_request + })) + .map_err(|e| format!("Failed to serialize TTS request: {}", e))?, + ) + .blob_bytes(vec![]); + + let response: Value = hyperware_process_lib::hyperapp::send(request) + .await + .map_err(|e| format!("Failed to call ttstt TTS: {:?}", e))?; + + // Handle the Result wrapper from ttstt + if let Some(ok_value) = response.get("Ok") { + // Success case - extract audio_data from TtsRes + let audio_base64 = ok_value + .get("audio_data") + .and_then(|v| v.as_str()) + .ok_or("Missing audio_data in TTS response".to_string())?; + + Ok(audio_base64.to_string()) + } else if let Some(err_value) = response.get("Err") { + // Error case + let error_msg = err_value.as_str().unwrap_or("Unknown error from ttstt"); + Err(format!("TTS failed: {}", error_msg)) + } else { + Err("Invalid response format from ttstt: expected Ok or Err".to_string()) + } + } + + // Helper function to convert audio to text using ttstt + async fn convert_audio_to_text( + &self, + audio_message: &MessageContent, + ) -> Result { + use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; + + // Create STT request for ttstt - matching the SttReq structure + #[derive(serde::Serialize)] + struct SttRequest { + audio_data: String, + provider: Option, + model: Option, + language: Option, + api_key: Option, + } + + let audio_data = match audio_message { + MessageContent::Audio(audio_data) => BASE64.encode(&audio_data), + MessageContent::BaseSixFourAudio(audio_data) => audio_data.to_string(), + MessageContent::Text(_) => { + return Err("convert_audio_to_text requires audio to convert".to_string()) + } + }; + + let stt_request = SttRequest { + audio_data, + provider: Some("OpenAI".to_string()), + model: Some("whisper-1".to_string()), + language: None, + api_key: None, // ttstt will accept requests from spider without auth + }; + + // Send request to ttstt using the stt endpoint + let request = Request::to(("our", TTSTT)) + .body( + serde_json::to_vec(&json!({ + "Stt": stt_request + })) + .unwrap(), + ) + .expects_response(30); // 30 second timeout for STT + + let response: Value = hyperware_process_lib::hyperapp::send(request) + .await + .map_err(|e| format!("Failed to call ttstt STT: {:?}", e))?; + + // Handle the Result wrapper from ttstt + // Response format is either {"Ok": {...}} or {"Err": "..."} + if let Some(ok_value) = response.get("Ok") { + // Success case - extract text from SttRes + let text = ok_value + .get("text") + .and_then(|t| t.as_str()) + .ok_or("Invalid STT response: missing text field")?; + Ok(text.to_string()) + } else if let Some(err_value) = response.get("Err") { + // Error case + let error_msg = err_value.as_str().unwrap_or("Unknown error from ttstt"); + Err(format!("STT failed: {}", error_msg)) + } else { + Err("Invalid response format from ttstt: expected Ok or Err".to_string()) + } + } + // Streaming version of chat for WebSocket clients async fn process_chat_request_with_streaming( &mut self, - request: ChatRequest, + request: ChatReq, channel_id: u32, - ) -> Result { + ) -> Result { // Create a cancellation flag for this request let cancel_flag = Arc::new(AtomicBool::new(false)); self.active_chat_cancellation @@ -1649,9 +1750,9 @@ impl SpiderState { // Internal chat processing logic shared by HTTP and WebSocket async fn process_chat_internal( &mut self, - request: ChatRequest, + request: ChatReq, channel_id: Option, - ) -> Result { + ) -> Result { // This is a refactored version of the chat logic that can send WebSocket updates // For now, just call the regular chat method // TODO: Refactor the chat method to use this shared logic @@ -1742,8 +1843,103 @@ impl SpiderState { } }; + // Process messages to convert any audio content to text + let mut processed_messages = Vec::new(); + for mut message in request.messages.clone() { + match &message.content { + MessageContent::Audio(_) | MessageContent::BaseSixFourAudio(_) => { + // Convert audio to text using ttstt + match self.convert_audio_to_text(&message.content).await { + Ok(text) => { + println!("Spider: Converted audio to text: {}", text); + message.content = MessageContent::Text(text); + } + Err(e) => { + return Err(format!("Failed to convert audio to text: {}", e)); + } + } + } + MessageContent::Text(_) => { + // Text content, no conversion needed + } + } + processed_messages.push(message); + } + + // Determine whether to reply with TTS or not + let user_prompt = processed_messages.clone(); + let user_prompt = user_prompt + .last() + .and_then(|m| m.content.as_text()) + .unwrap_or(""); + let metadata = request.metadata.clone().unwrap_or(ConversationMetadata { + start_time: Utc::now().to_rfc3339(), + client: "unknown".to_string(), + from_stt: false, + }); + let is_response_tts = if !metadata.from_stt { + false + } else { + // Ask the LLM whether to respond with text or audio + let decision_prompt = format!( + "Decide whether to reply with text or audio.\n\n\ + Always choose audio EXCEPT when user EXPLICITLY indicates they want text. \ + Examples of user wanting text: \ + - mentions \"dictating\"\n\ + - mentions \"transcribing\"\n\ + - mentions \"put this in my clipboard\"\n\n\ + User's last message: {user_prompt}\n\n\ + If should reply with text, reply only with `text`. \ + If should reply with audio, reply only with `audio`.", + ); + + // Create a simple request to the LLM for the decision + let decision_messages = vec![Message { + role: "user".to_string(), + content: MessageContent::Text(decision_prompt), + tool_calls_json: None, + tool_results_json: None, + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + }]; + + let provider = create_llm_provider(&llm_provider, &api_key); + + // Get decision from LLM (no tools needed) + if let Ok(decision_response) = provider + .complete( + &decision_messages, + &[], // No tools needed for this decision + None, + 100, // Small max tokens + 0.3, // Low temperature for consistent decisions + ) + .await + { + if let Some(decision_text) = decision_response.content.as_text() { + let decision = decision_text.trim().to_lowercase(); + + decision == "audio" + } else { + false + } + } else { + false + } + }; + + if is_response_tts { + if let Some(last) = processed_messages.last_mut() { + if let MessageContent::Text(ref mut text) = last.content { + text.push_str("\n\nReply concisely unless the above explicitly says to not be concise."); + } + } + } + // Start the agentic loop - runs indefinitely until the agent stops making tool calls - let mut working_messages = request.messages.clone(); + let mut working_messages = processed_messages; let mut iteration_count = 0; let response = loop { @@ -1842,7 +2038,13 @@ impl SpiderState { // Check if the response contains tool calls println!("[DEBUG] LLM response received:"); - println!("[DEBUG] - content: {}", llm_response.content); + println!( + "[DEBUG] - content: {}", + match &llm_response.content { + MessageContent::Text(t) => t.as_str(), + MessageContent::Audio(_) | MessageContent::BaseSixFourAudio(_) => "