From dbbee1cdbfb360e0c78e858136e5f4bb98a3dcd3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 12 May 2026 22:06:44 -0500 Subject: [PATCH] add encrypted Nostr session sharing (#8922) Signed-off-by: callebtc <93376500+callebtc@users.noreply.github.com> Signed-off-by: Douwe Osinga Signed-off-by: Michael Neale Co-authored-by: Douwe Osinga Co-authored-by: Michael Neale --- Cargo.lock | 364 ++++++++++++++++- crates/goose-cli/src/cli.rs | 37 +- crates/goose-cli/src/commands/session.rs | 47 ++- crates/goose-server/src/openapi.rs | 5 + crates/goose-server/src/routes/session.rs | 104 +++++ crates/goose/Cargo.toml | 5 + crates/goose/src/session/mod.rs | 1 + crates/goose/src/session/nostr_share.rs | 370 ++++++++++++++++++ ui/desktop/openapi.json | 147 +++++++ ui/desktop/src/App.tsx | 27 +- ui/desktop/src/api/index.ts | 4 +- ui/desktop/src/api/sdk.gen.ts | 20 +- ui/desktop/src/api/types.gen.ts | 82 ++++ .../components/sessions/SessionListView.tsx | 231 ++++++++++- ui/desktop/src/i18n/messages/en.json | 36 ++ ui/desktop/src/main.ts | 4 +- ui/desktop/src/sessionLinks.ts | 12 + 17 files changed, 1469 insertions(+), 27 deletions(-) create mode 100644 crates/goose/src/session/nostr_share.rs diff --git a/Cargo.lock b/Cargo.lock index a015086eb6..071546dd6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -379,6 +389,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-utility" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" +dependencies = [ + "futures-util", + "gloo-timers", + "tokio", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-wsocket" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069" +dependencies = [ + "async-utility", + "futures", + "futures-util", + "js-sys", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-tungstenite 0.26.2", + "url", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "atoi" version = "2.0.0" @@ -388,6 +429,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-destructor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1014,6 +1061,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + [[package]] name = "better_scoped_tls" version = "1.0.1" @@ -1315,6 +1368,17 @@ dependencies = [ "serde", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "serde", + "unicode-normalization", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1336,6 +1400,23 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1793,6 +1874,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.0" @@ -1804,6 +1896,19 @@ dependencies = [ "rand_core 0.10.0", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.44" @@ -1826,6 +1931,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -2409,6 +2515,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -4362,6 +4469,18 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "goose" version = "1.34.0" @@ -4417,6 +4536,8 @@ dependencies = [ "minijinja", "mockall", "nanoid", + "nostr", + "nostr-sdk", "oauth2", "once_cell", "opentelemetry", @@ -4437,6 +4558,7 @@ dependencies = [ "reqwest 0.13.2", "rmcp", "rubato", + "rustls", "schemars 1.2.1", "sec1", "serde", @@ -4803,6 +4925,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "hipstr" version = "0.6.0" @@ -5405,6 +5536,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -6208,6 +6351,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "negentropy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -6296,6 +6445,83 @@ dependencies = [ "nom 8.0.0", ] +[[package]] +name = "nostr" +version = "0.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" +dependencies = [ + "base64 0.22.1", + "bech32", + "bip39", + "bitcoin_hashes", + "cbc", + "chacha20 0.9.1", + "chacha20poly1305", + "getrandom 0.2.17", + "hex", + "instant", + "scrypt", + "secp256k1", + "serde", + "serde_json", + "unicode-normalization", + "url", +] + +[[package]] +name = "nostr-database" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" +dependencies = [ + "lru", + "nostr", + "tokio", +] + +[[package]] +name = "nostr-gossip" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" +dependencies = [ + "nostr", +] + +[[package]] +name = "nostr-relay-pool" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" +dependencies = [ + "async-utility", + "async-wsocket", + "atomic-destructor", + "hex", + "lru", + "negentropy", + "nostr", + "nostr-database", + "tokio", + "tracing", +] + +[[package]] +name = "nostr-sdk" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" +dependencies = [ + "async-utility", + "nostr", + "nostr-database", + "nostr-gossip", + "nostr-relay-pool", + "tokio", + "tracing", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -6616,6 +6842,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "5.3.4" @@ -6857,6 +7089,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -6890,6 +7133,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pctx_code_execution_runtime" version = "0.2.0" @@ -7257,6 +7510,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -7693,7 +7957,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "chacha20", + "chacha20 0.10.0", "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -8398,6 +8662,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -8500,6 +8773,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sdd" version = "3.0.10" @@ -8520,6 +8805,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "rand 0.8.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -10723,6 +11028,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -10734,6 +11051,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite 0.26.2", + "webpki-roots 0.26.11", +] + [[package]] name = "tokio-tungstenite" version = "0.28.0" @@ -11204,6 +11537,25 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.28.0" @@ -11443,6 +11795,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index b8a34b30e4..9691320427 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -543,6 +543,28 @@ enum SessionCommand { default_value = "markdown" )] format: String, + + #[arg( + long = "nostr", + help = "Publish the JSON session export as an encrypted Nostr event and print a Goose share link" + )] + nostr: bool, + + #[arg( + long = "relay", + value_name = "RELAY", + help = "Nostr relay URL to publish to (can be specified multiple times)", + action = clap::ArgAction::Append + )] + relays: Vec, + }, + #[command(about = "Import a session from JSON or an encrypted Nostr share link")] + Import { + #[arg(help = "Path to a JSON session export, or a goose://sessions/nostr share link")] + input: String, + + #[arg(long = "nostr", help = "Treat input as an encrypted Nostr share link")] + nostr: bool, }, #[command(name = "diagnostics")] Diagnostics { @@ -1227,6 +1249,8 @@ async fn handle_session_subcommand(command: SessionCommand) -> Result<()> { identifier, output, format, + nostr, + relays, } => { let session_manager = SessionManager::instance(); let session_identifier = if let Some(id) = identifier { @@ -1244,8 +1268,17 @@ async fn handle_session_subcommand(command: SessionCommand) -> Result<()> { } } }; - crate::commands::session::handle_session_export(session_identifier, output, format) - .await?; + crate::commands::session::handle_session_export( + session_identifier, + output, + format, + nostr, + relays, + ) + .await?; + } + SessionCommand::Import { input, nostr } => { + crate::commands::session::handle_session_import(input, nostr).await?; } SessionCommand::Diagnostics { identifier, output } => { let session_manager = SessionManager::instance(); diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 769ec523b8..9990d376e8 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -3,7 +3,8 @@ use anyhow::{Context, Result}; use cliclack::{confirm, multiselect, select}; use etcetera::home_dir; -use goose::session::{generate_diagnostics, Session, SessionManager}; +use goose::config::Config; +use goose::session::{generate_diagnostics, nostr_share, Session, SessionManager, SessionType}; use goose::utils::safe_truncate; use regex::Regex; use std::fs; @@ -216,6 +217,8 @@ pub async fn handle_session_export( session_id: String, output_path: Option, format: String, + nostr: bool, + relays: Vec, ) -> Result<()> { let session_manager = SessionManager::instance(); let session = match session_manager.get_session(&session_id, true).await { @@ -241,6 +244,29 @@ pub async fn handle_session_export( _ => return Err(anyhow::anyhow!("Unsupported format: {}", format)), }; + if nostr { + if format != "json" { + return Err(anyhow::anyhow!( + "Nostr session sharing only supports --format json" + )); + } + if output_path.is_some() { + return Err(anyhow::anyhow!( + "Nostr session sharing cannot be combined with --output" + )); + } + + let relays = nostr_share::resolve_relays(relays, Config::global()); + let share = nostr_share::publish_session_json(&output, relays).await?; + println!("Session published to Nostr relays:"); + for relay in &share.relays { + println!("- {}", relay); + } + println!("\nShare link:"); + println!("{}", share.deeplink); + return Ok(()); + } + if let Some(output_path) = output_path { fs::write(&output_path, output).with_context(|| { format!("Failed to write to output file: {}", output_path.display()) @@ -253,6 +279,25 @@ pub async fn handle_session_export( Ok(()) } +pub async fn handle_session_import(input: String, nostr: bool) -> Result<()> { + let json = if nostr || input.starts_with("goose://sessions/nostr") { + nostr_share::import_session_json_from_deeplink(&input).await? + } else { + fs::read_to_string(&input) + .with_context(|| format!("Failed to read session import file: {input}"))? + }; + + let session_manager = SessionManager::instance(); + let session = session_manager + .import_session(&json, Some(SessionType::User)) + .await?; + + println!("Session imported:"); + println!("{} - {}", session.id, session.name); + + Ok(()) +} + pub async fn handle_diagnostics(session_id: &str, output_path: Option) -> Result<()> { println!( "Generating diagnostics bundle for session '{}'...", diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 23aae67664..ae33e32b69 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -443,6 +443,8 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::session::delete_session, super::routes::session::export_session, super::routes::session::import_session, + super::routes::session::share_session_nostr, + super::routes::session::import_session_nostr, super::routes::session::update_session_user_recipe_values, super::routes::session::fork_session, super::routes::session::get_session_extensions, @@ -512,6 +514,9 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::session_events::SessionReplyResponse, super::routes::session_events::CancelRequest, super::routes::session::ImportSessionRequest, + super::routes::session::ShareSessionNostrRequest, + super::routes::session::ShareSessionNostrResponse, + super::routes::session::ImportSessionNostrRequest, super::routes::session::SessionListResponse, super::routes::session::UpdateSessionNameRequest, super::routes::session::UpdateSessionUserRecipeValuesRequest, diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index d83194e40b..5b0d89a88a 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -11,6 +11,7 @@ use axum::{ }; use goose::agents::ExtensionConfig; use goose::recipe::Recipe; +use goose::session::nostr_share; use goose::session::session_manager::{SessionInsights, SessionType}; use goose::session::{EnabledExtensionsState, Session}; use serde::{Deserialize, Serialize}; @@ -50,6 +51,28 @@ pub struct ImportSessionRequest { json: String, } +#[derive(Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ShareSessionNostrRequest { + #[serde(default)] + relays: Vec, +} + +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ShareSessionNostrResponse { + deeplink: String, + nevent: String, + event_id: String, + relays: Vec, +} + +#[derive(Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ImportSessionNostrRequest { + deeplink: String, +} + #[derive(Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ForkRequest { @@ -364,6 +387,79 @@ async fn import_session( Ok(Json(session)) } +#[utoipa::path( + post, + path = "/sessions/{session_id}/share/nostr", + request_body = ShareSessionNostrRequest, + params( + ("session_id" = String, Path, description = "Unique identifier for the session") + ), + responses( + (status = 200, description = "Session shared to Nostr successfully", body = ShareSessionNostrResponse), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 404, description = "Session not found"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Session Management" +)] +async fn share_session_nostr( + State(state): State>, + Path(session_id): Path, + Json(request): Json, +) -> Result, StatusCode> { + let exported = state + .session_manager() + .export_session(&session_id) + .await + .map_err(|_| StatusCode::NOT_FOUND)?; + + let relays = nostr_share::resolve_relays(request.relays, goose::config::Config::global()); + let share = nostr_share::publish_session_json(&exported, relays) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(ShareSessionNostrResponse { + deeplink: share.deeplink, + nevent: share.nevent, + event_id: share.event_id, + relays: share.relays, + })) +} + +#[utoipa::path( + post, + path = "/sessions/import/nostr", + request_body = ImportSessionNostrRequest, + responses( + (status = 200, description = "Nostr shared session imported successfully", body = Session), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 400, description = "Bad request - Invalid Nostr share link"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Session Management" +)] +async fn import_session_nostr( + State(state): State>, + Json(request): Json, +) -> Result, StatusCode> { + let json = nostr_share::import_session_json_from_deeplink(&request.deeplink) + .await + .map_err(|_| StatusCode::BAD_REQUEST)?; + let session = state + .session_manager() + .import_session(&json, Some(SessionType::User)) + .await + .map_err(|_| StatusCode::BAD_REQUEST)?; + + Ok(Json(session)) +} + #[utoipa::path( post, path = "/sessions/{session_id}/fork", @@ -505,10 +601,18 @@ pub fn routes(state: Arc) -> Router { .route("/sessions/{session_id}", get(get_session)) .route("/sessions/{session_id}", delete(delete_session)) .route("/sessions/{session_id}/export", get(export_session)) + .route( + "/sessions/{session_id}/share/nostr", + post(share_session_nostr).layer(DefaultBodyLimit::max(25 * 1024 * 1024)), + ) .route( "/sessions/import", post(import_session).layer(DefaultBodyLimit::max(25 * 1024 * 1024)), ) + .route( + "/sessions/import/nostr", + post(import_session_nostr).layer(DefaultBodyLimit::max(25 * 1024 * 1024)), + ) .route("/sessions/insights", get(get_session_insights)) .route("/sessions/{session_id}/name", put(update_session_name)) .route( diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 95ac5f73e3..c11b0754c6 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -39,6 +39,7 @@ aws-providers = [ cuda = ["local-inference", "candle-core/cuda", "candle-nn/cuda", "llama-cpp-2/cuda"] vulkan = ["local-inference", "llama-cpp-2/vulkan"] rustls-tls = [ + "dep:rustls", "reqwest/rustls", "rmcp/reqwest", "sqlx/runtime-tokio-rustls", @@ -59,6 +60,7 @@ native-tls = [ "oauth2/native-tls", ] + [lints] workspace = true @@ -194,6 +196,9 @@ goose-acp-macros = { path = "../goose-acp-macros" } tower-http = { workspace = true, features = ["cors"] } http-body-util = "0.1.3" process-wrap = { version = "9.1.0", features = ["std"] } +nostr = { version = "0.44.2", features = ["nip44"] } +nostr-sdk = { version = "0.44.1", features = ["nip44"] } +rustls = { version = "0.23", features = ["aws_lc_rs"], optional = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/goose/src/session/mod.rs b/crates/goose/src/session/mod.rs index 64dfdedcc4..bf58fa8970 100644 --- a/crates/goose/src/session/mod.rs +++ b/crates/goose/src/session/mod.rs @@ -2,6 +2,7 @@ mod chat_history_search; mod diagnostics; pub mod extension_data; mod legacy; +pub mod nostr_share; pub mod session_manager; pub use diagnostics::{ diff --git a/crates/goose/src/session/nostr_share.rs b/crates/goose/src/session/nostr_share.rs new file mode 100644 index 0000000000..faf5d038fb --- /dev/null +++ b/crates/goose/src/session/nostr_share.rs @@ -0,0 +1,370 @@ +use std::time::Duration; + +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use nostr::nips::nip19::{FromBech32, Nip19Event, ToBech32}; +use nostr::nips::nip44; +use nostr::prelude::*; +use nostr_sdk::Client; + +use crate::config::{Config, ConfigError}; + +pub const EVENT_KIND: u16 = 30278; +pub const CONFIG_RELAYS_KEY: &str = "GOOSE_NOSTR_RELAYS"; + +const DEFAULT_RELAYS: &[&str] = &[ + "wss://relay.damus.io", + "wss://relay.primal.net", + "wss://nos.lol", + "wss://relay.nostr.band", +]; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NostrShare { + pub deeplink: String, + pub nevent: String, + pub event_id: String, + pub relays: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedShareLink { + pub nevent: String, + pub decryption_key: String, +} + +#[async_trait] +pub trait NostrPublisher { + async fn publish(&self, event: Event, relays: &[String]) -> Result<()>; +} + +#[async_trait] +pub trait NostrFetcher { + async fn fetch(&self, event_id: EventId, relays: &[String]) -> Result; +} + +pub struct LiveNostrClient; + +#[async_trait] +impl NostrPublisher for LiveNostrClient { + async fn publish(&self, event: Event, relays: &[String]) -> Result<()> { + install_rustls_crypto_provider(); + let client = Client::default(); + for relay in relays { + client + .add_relay(relay) + .await + .with_context(|| format!("Failed to add relay {relay}"))?; + } + + client.try_connect(Duration::from_secs(8)).await; + let output = client + .send_event_to(relays.iter().map(String::as_str), &event) + .await + .context("Failed to publish session to Nostr relays")?; + client.shutdown().await; + + if output.success.is_empty() { + return Err(anyhow!( + "Failed to publish session to any Nostr relay: {:?}", + output.failed + )); + } + + Ok(()) + } +} + +#[async_trait] +impl NostrFetcher for LiveNostrClient { + async fn fetch(&self, event_id: EventId, relays: &[String]) -> Result { + install_rustls_crypto_provider(); + let client = Client::default(); + for relay in relays { + client + .add_relay(relay) + .await + .with_context(|| format!("Failed to add relay {relay}"))?; + } + + client.try_connect(Duration::from_secs(8)).await; + let filter = Filter::new() + .id(event_id) + .kind(Kind::Custom(EVENT_KIND)) + .limit(1); + let events = client + .fetch_events_from( + relays.iter().map(String::as_str), + filter, + Duration::from_secs(10), + ) + .await + .context("Failed to fetch shared session from Nostr relays")?; + client.shutdown().await; + + events + .into_iter() + .next() + .ok_or_else(|| anyhow!("Shared session event not found")) + } +} + +#[cfg(feature = "rustls-tls")] +fn install_rustls_crypto_provider() { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); +} + +#[cfg(not(feature = "rustls-tls"))] +fn install_rustls_crypto_provider() {} + +pub fn default_relays() -> Vec { + DEFAULT_RELAYS + .iter() + .map(|relay| relay.to_string()) + .collect() +} + +pub fn relays_from_config(config: &Config) -> Vec { + match config.get_param::>(CONFIG_RELAYS_KEY) { + Ok(relays) if !relays.is_empty() => normalize_relays(relays), + Err(ConfigError::NotFound(_)) => default_relays(), + _ => default_relays(), + } +} + +pub fn resolve_relays(cli_relays: Vec, config: &Config) -> Vec { + if cli_relays.is_empty() { + relays_from_config(config) + } else { + normalize_relays(cli_relays) + } +} + +pub async fn publish_session_json(session_json: &str, relays: Vec) -> Result { + publish_session_json_with(session_json, relays, &LiveNostrClient).await +} + +pub async fn publish_session_json_with

( + session_json: &str, + relays: Vec, + publisher: &P, +) -> Result +where + P: NostrPublisher + Sync, +{ + let relays = normalize_relays(relays); + if relays.is_empty() { + return Err(anyhow!("At least one Nostr relay is required")); + } + let relay_urls = relays + .iter() + .map(|relay| RelayUrl::parse(relay)) + .collect::, _>>()?; + + let publish_keys = Keys::generate(); + let encryption_key = SecretKey::generate(); + let encryption_keys = Keys::new(encryption_key.clone()); + let encrypted = nip44::encrypt( + &encryption_key, + &encryption_keys.public_key(), + session_json, + nip44::Version::V2, + )?; + + let event = EventBuilder::new(Kind::Custom(EVENT_KIND), encrypted) + .tag(Tag::identifier(format!( + "goose-session-{}", + uuid::Uuid::now_v7() + ))) + .tag(Tag::parse(["client", "goose"])?) + .sign_with_keys(&publish_keys)?; + + publisher.publish(event.clone(), &relays).await?; + + let nevent = Nip19Event::new(event.id) + .author(event.pubkey) + .kind(Kind::Custom(EVENT_KIND)) + .relays(relay_urls) + .to_bech32()?; + let decryption_key = encryption_key.to_secret_hex(); + let deeplink = build_deeplink(&nevent, &decryption_key); + + Ok(NostrShare { + deeplink, + nevent, + event_id: event.id.to_hex(), + relays, + }) +} + +pub async fn import_session_json_from_deeplink(deeplink: &str) -> Result { + import_session_json_from_deeplink_with(deeplink, &LiveNostrClient).await +} + +pub async fn import_session_json_from_deeplink_with( + deeplink: &str, + fetcher: &F, +) -> Result +where + F: NostrFetcher + Sync, +{ + let ParsedShareLink { + nevent, + decryption_key, + } = parse_deeplink(deeplink)?; + let event_ref = Nip19Event::from_bech32(&nevent)?; + let relays = event_ref + .relays + .iter() + .map(ToString::to_string) + .collect::>(); + + if relays.is_empty() { + return Err(anyhow!("Shared session link does not include any relays")); + } + + let event = fetcher.fetch(event_ref.event_id, &relays).await?; + if event.kind != Kind::Custom(EVENT_KIND) { + return Err(anyhow!( + "Unexpected Nostr event kind: {}", + u16::from(event.kind) + )); + } + + let secret_key = SecretKey::parse(&decryption_key)?; + let encryption_keys = Keys::new(secret_key.clone()); + nip44::decrypt(&secret_key, &encryption_keys.public_key(), event.content).map_err(Into::into) +} + +pub fn build_deeplink(nevent: &str, decryption_key: &str) -> String { + format!( + "goose://sessions/nostr?nevent={}&key={}", + urlencoding::encode(nevent), + urlencoding::encode(decryption_key) + ) +} + +pub fn parse_deeplink(deeplink: &str) -> Result { + let parsed = url::Url::parse(deeplink).context("Invalid Goose session share link")?; + if parsed.scheme() != "goose" + || parsed.host_str() != Some("sessions") + || parsed.path() != "/nostr" + { + return Err(anyhow!("Invalid Goose Nostr session share link")); + } + + let nevent = parsed + .query_pairs() + .find_map(|(key, value)| (key == "nevent").then(|| value.into_owned())) + .ok_or_else(|| anyhow!("Missing nevent parameter"))?; + let decryption_key = parsed + .query_pairs() + .find_map(|(key, value)| (key == "key").then(|| value.into_owned())) + .ok_or_else(|| anyhow!("Missing decryption key parameter"))?; + + Ok(ParsedShareLink { + nevent, + decryption_key, + }) +} + +fn normalize_relays(relays: Vec) -> Vec { + let mut normalized = Vec::new(); + for relay in relays { + let relay = relay.trim(); + if relay.is_empty() || normalized.iter().any(|existing| existing == relay) { + continue; + } + normalized.push(relay.to_string()); + } + normalized +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + + struct RecordingPublisher { + event: Arc>>, + relays: Arc>>, + } + + #[async_trait] + impl NostrPublisher for RecordingPublisher { + async fn publish(&self, event: Event, relays: &[String]) -> Result<()> { + *self.event.lock().unwrap() = Some(event); + *self.relays.lock().unwrap() = relays.to_vec(); + Ok(()) + } + } + + struct StaticFetcher(Event); + + #[async_trait] + impl NostrFetcher for StaticFetcher { + async fn fetch(&self, _event_id: EventId, _relays: &[String]) -> Result { + Ok(self.0.clone()) + } + } + + #[tokio::test] + async fn publish_builds_deeplink_and_encrypted_kind_30278_event() { + let event = Arc::new(Mutex::new(None)); + let relays = Arc::new(Mutex::new(Vec::new())); + let publisher = RecordingPublisher { + event: event.clone(), + relays: relays.clone(), + }; + + let share = publish_session_json_with( + r#"{"id":"session-id","conversation":{"messages":[]}}"#, + vec!["wss://relay.example".to_string()], + &publisher, + ) + .await + .unwrap(); + + assert!(share.deeplink.starts_with("goose://sessions/nostr?")); + assert!(share.nevent.starts_with("nevent1")); + assert_eq!(share.relays, vec!["wss://relay.example"]); + assert_eq!(*relays.lock().unwrap(), vec!["wss://relay.example"]); + + let event = event.lock().unwrap().clone().unwrap(); + assert_eq!(event.kind, Kind::Custom(EVENT_KIND)); + assert_ne!( + event.content, + r#"{"id":"session-id","conversation":{"messages":[]}}"# + ); + } + + #[tokio::test] + async fn publish_and_import_round_trips_session_json() { + let event = Arc::new(Mutex::new(None)); + let publisher = RecordingPublisher { + event: event.clone(), + relays: Arc::new(Mutex::new(Vec::new())), + }; + let json = r#"{"id":"session-id","name":"shared"}"#; + + let share = + publish_session_json_with(json, vec!["wss://relay.example".to_string()], &publisher) + .await + .unwrap(); + + let fetched_event = event.lock().unwrap().clone().unwrap(); + let imported = + import_session_json_from_deeplink_with(&share.deeplink, &StaticFetcher(fetched_event)) + .await + .unwrap(); + + assert_eq!(imported, json); + } + + #[test] + fn parses_deeplink() { + let parsed = parse_deeplink("goose://sessions/nostr?nevent=abc&key=def").unwrap(); + assert_eq!(parsed.nevent, "abc"); + assert_eq!(parsed.decryption_key, "def"); + } +} diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index a8efa20204..3f0a16d6d8 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3145,6 +3145,50 @@ ] } }, + "/sessions/import/nostr": { + "post": { + "tags": [ + "Session Management" + ], + "operationId": "import_session_nostr", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportSessionNostrRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Nostr shared session imported successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" + } + } + } + }, + "400": { + "description": "Bad request - Invalid Nostr share link" + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/sessions/insights": { "get": { "tags": [ @@ -3656,6 +3700,61 @@ ] } }, + "/sessions/{session_id}/share/nostr": { + "post": { + "tags": [ + "Session Management" + ], + "operationId": "share_session_nostr", + "parameters": [ + { + "name": "session_id", + "in": "path", + "description": "Unique identifier for the session", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShareSessionNostrRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Session shared to Nostr successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShareSessionNostrResponse" + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "404": { + "description": "Session not found" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/sessions/{session_id}/user_recipe_values": { "put": { "tags": [ @@ -5625,6 +5724,17 @@ } } }, + "ImportSessionNostrRequest": { + "type": "object", + "required": [ + "deeplink" + ], + "properties": { + "deeplink": { + "type": "string" + } + } + }, "ImportSessionRequest": { "type": "object", "required": [ @@ -8105,6 +8215,43 @@ } } }, + "ShareSessionNostrRequest": { + "type": "object", + "properties": { + "relays": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ShareSessionNostrResponse": { + "type": "object", + "required": [ + "deeplink", + "nevent", + "eventId", + "relays" + ], + "properties": { + "deeplink": { + "type": "string" + }, + "eventId": { + "type": "string" + }, + "nevent": { + "type": "string" + }, + "relays": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "SlashCommand": { "type": "object", "required": [ diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index ca32d04d12..e53dcde476 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -8,7 +8,7 @@ import { useLocation, useSearchParams, } from 'react-router-dom'; -import { openSharedSessionFromDeepLink } from './sessionLinks'; +import { openSharedSessionFromDeepLink, importNostrSessionFromDeepLink } from './sessionLinks'; import { type SharedSessionDetails } from './sharedSessions'; import { ErrorUI } from './components/ErrorBoundary'; import { ExtensionInstallModal } from './components/ExtensionInstallModal'; @@ -428,6 +428,11 @@ export function AppInner() { setIsLoadingSharedSession(true); setSharedSessionError(null); try { + if (link.startsWith('goose://sessions/nostr')) { + await importNostrSessionFromDeepLink(link); + navigate('/sessions'); + return; + } await openSharedSessionFromDeepLink(link, (_view: View, options?: ViewOptions) => { navigate('/shared-session', { state: options }); }); @@ -438,14 +443,18 @@ export function AppInner() { action: 'open_shared_session', recoverable: true, }); - // Navigate to shared session view with error - const shareToken = link.replace('goose://sessions/', ''); - const options = { - sessionDetails: null, - error: errorMessage(error, 'Unknown error'), - shareToken, - }; - navigate('/shared-session', { state: options }); + if (link.startsWith('goose://sessions/nostr')) { + toast.error(`Failed to import Nostr session: ${errorMessage(error, 'Unknown error')}`); + navigate('/sessions'); + } else { + const shareToken = link.replace('goose://sessions/', ''); + const options = { + sessionDetails: null, + error: errorMessage(error, 'Unknown error'), + shareToken, + }; + navigate('/shared-session', { state: options }); + } } finally { setIsLoadingSharedSession(false); } diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index c941a2af88..fd1811a2c9 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, cleanupProviderCache, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getFeatures, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, syncFeaturedModels, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, CallToolData, CallToolError, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponse, CleanupProviderCacheResponses, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, FeaturesResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetFeaturesData, GetFeaturesResponse, GetFeaturesResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, IconTheme, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export { addExtension, agentAddExtension, agentRemoveExtension, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, cleanupProviderCache, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getFeatures, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, importSessionNostr, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, shareSessionNostr, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, syncFeaturedModels, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, CallToolData, CallToolError, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponse, CleanupProviderCacheResponses, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, FeaturesResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetFeaturesData, GetFeaturesResponse, GetFeaturesResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, IconTheme, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionNostrData, ImportSessionNostrErrors, ImportSessionNostrRequest, ImportSessionNostrResponse, ImportSessionNostrResponses, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, ShareSessionNostrData, ShareSessionNostrErrors, ShareSessionNostrRequest, ShareSessionNostrResponse, ShareSessionNostrResponse2, ShareSessionNostrResponses, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 920160ebf8..2870da539d 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponses, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetFeaturesData, GetFeaturesResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponses, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetFeaturesData, GetFeaturesResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionNostrData, ImportSessionNostrErrors, ImportSessionNostrResponses, ImportSessionResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, ShareSessionNostrData, ShareSessionNostrErrors, ShareSessionNostrResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -494,6 +494,15 @@ export const importSession = (options: Opt } }); +export const importSessionNostr = (options: Options) => (options.client ?? client).post({ + url: '/sessions/import/nostr', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + export const getSessionInsights = (options?: Options) => (options?.client ?? client).get({ url: '/sessions/insights', ...options }); export const searchSessions = (options: Options) => (options.client ?? client).get({ url: '/sessions/search', ...options }); @@ -544,6 +553,15 @@ export const updateSessionName = (options: } }); +export const shareSessionNostr = (options: Options) => (options.client ?? client).post({ + url: '/sessions/{session_id}/share/nostr', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + export const updateSessionUserRecipeValues = (options: Options) => (options.client ?? client).put({ url: '/sessions/{session_id}/user_recipe_values', ...options, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 0f398a0e0e..5f410551c5 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -593,6 +593,10 @@ export type ImportAppResponse = { name: string; }; +export type ImportSessionNostrRequest = { + deeplink: string; +}; + export type ImportSessionRequest = { json: string; }; @@ -1368,6 +1372,17 @@ export type SetupResponse = { success: boolean; }; +export type ShareSessionNostrRequest = { + relays?: Array; +}; + +export type ShareSessionNostrResponse = { + deeplink: string; + eventId: string; + nevent: string; + relays: Array; +}; + export type SlashCommand = { command: string; command_type: CommandType; @@ -4091,6 +4106,37 @@ export type ImportSessionResponses = { export type ImportSessionResponse = ImportSessionResponses[keyof ImportSessionResponses]; +export type ImportSessionNostrData = { + body: ImportSessionNostrRequest; + path?: never; + query?: never; + url: '/sessions/import/nostr'; +}; + +export type ImportSessionNostrErrors = { + /** + * Bad request - Invalid Nostr share link + */ + 400: unknown; + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type ImportSessionNostrResponses = { + /** + * Nostr shared session imported successfully + */ + 200: Session; +}; + +export type ImportSessionNostrResponse = ImportSessionNostrResponses[keyof ImportSessionNostrResponses]; + export type GetSessionInsightsData = { body?: never; path?: never; @@ -4473,6 +4519,42 @@ export type UpdateSessionNameResponses = { 200: unknown; }; +export type ShareSessionNostrData = { + body: ShareSessionNostrRequest; + path: { + /** + * Unique identifier for the session + */ + session_id: string; + }; + query?: never; + url: '/sessions/{session_id}/share/nostr'; +}; + +export type ShareSessionNostrErrors = { + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Session not found + */ + 404: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type ShareSessionNostrResponses = { + /** + * Session shared to Nostr successfully + */ + 200: ShareSessionNostrResponse; +}; + +export type ShareSessionNostrResponse2 = ShareSessionNostrResponses[keyof ShareSessionNostrResponses]; + export type UpdateSessionUserRecipeValuesData = { body: UpdateSessionUserRecipeValuesRequest; path: { diff --git a/ui/desktop/src/components/sessions/SessionListView.tsx b/ui/desktop/src/components/sessions/SessionListView.tsx index 3f86e12c1e..66616ba7f5 100644 --- a/ui/desktop/src/components/sessions/SessionListView.tsx +++ b/ui/desktop/src/components/sessions/SessionListView.tsx @@ -11,6 +11,8 @@ import { Trash2, Download, Upload, + Share2, + LoaderCircle, ExternalLink, Copy, Puzzle, @@ -28,18 +30,29 @@ import { Skeleton } from '../ui/skeleton'; import { toast } from 'react-toastify'; import { ConfirmationModal } from '../ui/ConfirmationModal'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '../ui/dialog'; import { deleteSession, exportSession, forkSession, importSession, + importSessionNostr, listSessions, searchSessions, + shareSessionNostr, Session, updateSessionName, ExtensionConfig, ExtensionData, } from '../../api'; +import { getTunnelStatus } from '../../api/sdk.gen'; import { formatExtensionName } from '../settings/extensions/subcomponents/ExtensionList'; import { getSearchShortcutText } from '../../utils/keyboardShortcuts'; import { shouldShowNewChatTitle } from '../../sessions'; @@ -55,6 +68,11 @@ const i18n = defineMessages({ sessionUpdateFailed: { id: 'sessions.toast.updateFailed', defaultMessage: 'Failed to update session description: {error}' }, chatHistory: { id: 'sessions.chatHistory', defaultMessage: 'Chat history' }, importSession: { id: 'sessions.import', defaultMessage: 'Import Session' }, + importNostrSession: { id: 'sessions.importNostr', defaultMessage: 'Import Link' }, + importNostrTitle: { id: 'sessions.importNostr.title', defaultMessage: 'Import Nostr Session' }, + importNostrDesc: { id: 'sessions.importNostr.description', defaultMessage: 'Paste a Goose Nostr share link to fetch, decrypt, and import the session.' }, + importNostrPlaceholder: { id: 'sessions.importNostr.placeholder', defaultMessage: 'goose://sessions/nostr?nevent=...&key=...' }, + importing: { id: 'sessions.importing', defaultMessage: 'Importing...' }, chatHistoryDesc: { id: 'sessions.chatHistoryDesc', defaultMessage: 'View and search your past conversations with Goose. {shortcut} to search.' }, searchPlaceholder: { id: 'sessions.searchPlaceholder', defaultMessage: 'Search history...' }, errorLoading: { id: 'sessions.error.loading', defaultMessage: 'Error Loading Sessions' }, @@ -73,12 +91,19 @@ const i18n = defineMessages({ importSuccess: { id: 'sessions.toast.imported', defaultMessage: 'Session imported successfully' }, importFailed: { id: 'sessions.toast.importFailed', defaultMessage: 'Failed to import session: {error}' }, exportSuccess: { id: 'sessions.toast.exported', defaultMessage: 'Session exported successfully' }, + shareNostrSuccess: { id: 'sessions.toast.shareNostr', defaultMessage: 'Encrypted Nostr share link created' }, + shareNostrFailed: { id: 'sessions.toast.shareNostrFailed', defaultMessage: 'Failed to create Nostr share link: {error}' }, + copied: { id: 'sessions.toast.copied', defaultMessage: 'Copied to clipboard' }, openInNewWindow: { id: 'sessions.action.openNewWindow', defaultMessage: 'Open in new window' }, editSessionName: { id: 'sessions.action.editName', defaultMessage: 'Edit session name' }, duplicateSession: { id: 'sessions.action.duplicate', defaultMessage: 'Duplicate session' }, deleteSession: { id: 'sessions.action.delete', defaultMessage: 'Delete session' }, exportSession: { id: 'sessions.action.export', defaultMessage: 'Export session' }, + shareNostrSession: { id: 'sessions.action.shareNostr', defaultMessage: 'Share encrypted Nostr link' }, extensions: { id: 'sessions.extensions', defaultMessage: 'Extensions:' }, + shareNostrTitle: { id: 'sessions.shareNostr.title', defaultMessage: 'Encrypted Nostr Share Link' }, + shareNostrDesc: { id: 'sessions.shareNostr.description', defaultMessage: 'Anyone with this link can fetch and decrypt the session. Treat it like a secret.' }, + close: { id: 'sessions.close', defaultMessage: 'Close' }, }); function getSessionExtensionNames(extensionData: ExtensionData): string[] { @@ -265,6 +290,14 @@ const SessionListView: React.FC = React.memo( const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); const [sessionToDelete, setSessionToDelete] = useState(null); + const [showImportLinkModal, setShowImportLinkModal] = useState(false); + const [nostrImportLink, setNostrImportLink] = useState(''); + const [isImportingNostr, setIsImportingNostr] = useState(false); + const [shareLink, setShareLink] = useState(''); + const [showShareLinkModal, setShowShareLinkModal] = useState(false); + const [sharingSessionId, setSharingSessionId] = useState(null); + const [nostrEnabled, setNostrEnabled] = useState(true); + // Search state for debouncing const [searchTerm, setSearchTerm] = useState(''); const [caseSensitive, setCaseSensitive] = useState(false); @@ -338,6 +371,17 @@ const SessionListView: React.FC = React.memo( loadSessions(); }, [loadSessions]); + // Hide Nostr sharing when tunnel is disabled (restricted/enterprise bundles) + useEffect(() => { + getTunnelStatus() + .then(({ data }) => { + if (data?.state === 'disabled') { + setNostrEnabled(false); + } + }) + .catch(() => {}); + }, []); + // Timing logic to prevent flicker between skeleton and content on initial load useEffect(() => { if (!isLoading && showSkeleton) { @@ -542,10 +586,62 @@ const SessionListView: React.FC = React.memo( toast.success(intl.formatMessage(i18n.exportSuccess)); }, [intl]); + const handleShareSessionNostr = useCallback( + async (session: Session, e: React.MouseEvent) => { + e.stopPropagation(); + setSharingSessionId(session.id); + try { + const response = await shareSessionNostr({ + path: { session_id: session.id }, + body: {}, + throwOnError: true, + }); + setShareLink(response.data.deeplink); + setShowShareLinkModal(true); + toast.success(intl.formatMessage(i18n.shareNostrSuccess)); + } catch (error) { + toast.error(intl.formatMessage(i18n.shareNostrFailed, { error: errorMessage(error, 'Unknown error') })); + } finally { + setSharingSessionId(null); + } + }, + [intl] + ); + const handleImportClick = useCallback(() => { fileInputRef.current?.click(); }, []); + const handleImportNostrLink = useCallback(async () => { + const deeplink = nostrImportLink.trim(); + if (!deeplink) return; + + setIsImportingNostr(true); + try { + await importSessionNostr({ + body: { deeplink }, + throwOnError: true, + }); + setNostrImportLink(''); + setShowImportLinkModal(false); + toast.success(intl.formatMessage(i18n.importSuccess)); + await loadSessions(); + } catch (error) { + toast.error(intl.formatMessage(i18n.importFailed, { error: errorMessage(error, 'Unknown error') })); + } finally { + setIsImportingNostr(false); + } + }, [intl, loadSessions, nostrImportLink]); + + const handleCopyShareLink = useCallback(async () => { + try { + await navigator.clipboard.writeText(shareLink); + toast.success(intl.formatMessage(i18n.copied)); + } catch (error) { + toast.error(`Failed to copy: ${errorMessage(error, 'Unknown error')}`); + } + }, [intl, shareLink]); + const handleImportSession = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -586,14 +682,18 @@ const SessionListView: React.FC = React.memo( onDuplicateClick, onDeleteClick, onExportClick, + onShareClick, onOpenInNewWindow, + isSharing, }: { session: Session; onEditClick: (session: Session) => void; onDuplicateClick: (session: Session) => void; onDeleteClick: (session: Session) => void; onExportClick: (session: Session, e: React.MouseEvent) => void; + onShareClick: (session: Session, e: React.MouseEvent) => void; onOpenInNewWindow: (session: Session, e: React.MouseEvent) => void; + isSharing: boolean; }) { const handleEditClick = useCallback( (e: React.MouseEvent) => { @@ -630,6 +730,13 @@ const SessionListView: React.FC = React.memo( [onExportClick, session] ); + const handleShareClick = useCallback( + (e: React.MouseEvent) => { + onShareClick(session, e); + }, + [onShareClick, session] + ); + const handleOpenInNewWindowClick = useCallback( (e: React.MouseEvent) => { onOpenInNewWindow(session, e); @@ -736,6 +843,20 @@ const SessionListView: React.FC = React.memo( > + {nostrEnabled && ( + + )} ); @@ -828,7 +949,9 @@ const SessionListView: React.FC = React.memo( onDuplicateClick={handleDuplicateSession} onDeleteClick={handleDeleteSession} onExportClick={handleExportSession} + onShareClick={handleShareSessionNostr} onOpenInNewWindow={handleOpenInNewWindow} + isSharing={sharingSessionId === session.id} /> ))} @@ -855,15 +978,28 @@ const SessionListView: React.FC = React.memo(

{intl.formatMessage(i18n.chatHistory)}

- +
+ {nostrEnabled && ( + + )} + +

{intl.formatMessage(i18n.chatHistoryDesc, { shortcut: getSearchShortcutText() })} @@ -957,6 +1093,83 @@ const SessionListView: React.FC = React.memo( onSave={handleModalSave} /> +

+ + + + + {intl.formatMessage(i18n.importNostrTitle)} + + {intl.formatMessage(i18n.importNostrDesc)} + + +