From 9759cc4ea3507cf849d07cc3891a5562c732bdc9 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 11:07:19 -0500 Subject: [PATCH 01/16] custom methods --- Cargo.lock | 1 + crates/goose-acp/Cargo.toml | 3 +- crates/goose-acp/src/custom_requests.rs | 151 +++++++++++++++ crates/goose-acp/src/lib.rs | 1 + crates/goose-acp/src/server.rs | 243 +++++++++++++++++++++++- 5 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 crates/goose-acp/src/custom_requests.rs diff --git a/Cargo.lock b/Cargo.lock index b351444a38b5..886878d4f460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4285,6 +4285,7 @@ dependencies = [ "regex", "rmcp 0.15.0", "sacp", + "schemars 1.2.1", "serde", "serde_json", "tempfile", diff --git a/crates/goose-acp/Cargo.toml b/crates/goose-acp/Cargo.toml index 261920bcd4aa..16f2569c51ae 100644 --- a/crates/goose-acp/Cargo.toml +++ b/crates/goose-acp/Cargo.toml @@ -33,13 +33,14 @@ url = { workspace = true } # HTTP server dependencies axum = { workspace = true, features = ["ws"] } clap = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } tower-http = { workspace = true, features = ["cors"] } tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } async-stream = { workspace = true } bytes = { workspace = true } http-body-util = "0.1.3" uuid = { workspace = true, features = ["v7"] } +schemars = { workspace = true, features = ["derive"] } [dev-dependencies] assert-json-diff = "2.0.2" diff --git a/crates/goose-acp/src/custom_requests.rs b/crates/goose-acp/src/custom_requests.rs new file mode 100644 index 000000000000..e8a10c3e22f2 --- /dev/null +++ b/crates/goose-acp/src/custom_requests.rs @@ -0,0 +1,151 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Agent: extensions +// --------------------------------------------------------------------------- + +/// Add an extension to an active session. +/// Method: `_agent/extensions/add` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct AddExtensionRequest { + pub session_id: String, + /// Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform). + pub config: serde_json::Value, +} + +/// Remove an extension from an active session. +/// Method: `_agent/extensions/remove` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct RemoveExtensionRequest { + pub session_id: String, + pub name: String, +} + +// --------------------------------------------------------------------------- +// Agent: tools +// --------------------------------------------------------------------------- + +/// List all tools available in a session. +/// Method: `_agent/tools` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetToolsRequest { + pub session_id: String, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub struct GetToolsResponse { + /// Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. + pub tools: Vec, +} + +// --------------------------------------------------------------------------- +// Agent: resource +// --------------------------------------------------------------------------- + +/// Read a resource from an extension. +/// Method: `_agent/resource/read` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ReadResourceRequest { + pub session_id: String, + pub uri: String, + pub extension_name: String, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub struct ReadResourceResponse { + /// The resource result from the extension (MCP ReadResourceResult). + pub result: serde_json::Value, +} + +// --------------------------------------------------------------------------- +// Agent: working directory +// --------------------------------------------------------------------------- + +/// Update the working directory for a session. +/// Method: `_agent/working_dir/update` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct UpdateWorkingDirRequest { + pub session_id: String, + pub working_dir: String, +} + +// --------------------------------------------------------------------------- +// Session management +// --------------------------------------------------------------------------- + +/// Get a session by ID. +/// Method: `_session/get` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetSessionRequest { + pub session_id: String, + #[serde(default)] + pub include_messages: bool, +} + +/// Get a session response. +#[derive(Debug, Serialize, JsonSchema)] +pub struct GetSessionResponse { + /// The session object with id, name, working_dir, timestamps, tokens, etc. + pub session: serde_json::Value, +} + +/// List all sessions. +/// Method: `_session/list` +#[derive(Debug, Serialize, JsonSchema)] +pub struct ListSessionsResponse { + pub sessions: Vec, +} + +/// Delete a session. +/// Method: `_session/delete` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct DeleteSessionRequest { + pub session_id: String, +} + +/// Export a session as a JSON string. +/// Method: `_session/export` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ExportSessionRequest { + pub session_id: String, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub struct ExportSessionResponse { + pub data: String, +} + +/// Import a session from a JSON string. +/// Method: `_session/import` +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ImportSessionRequest { + pub data: String, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub struct ImportSessionResponse { + /// The imported session object. + pub session: serde_json::Value, +} + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +/// List configured extensions and any warnings. +/// Method: `_config/extensions` +#[derive(Debug, Serialize, JsonSchema)] +pub struct GetExtensionsResponse { + /// Array of ExtensionEntry objects with `enabled` flag and config details. + pub extensions: Vec, + pub warnings: Vec, +} + +// --------------------------------------------------------------------------- +// Shared empty response +// --------------------------------------------------------------------------- + +/// Empty success response for operations that return no data. +#[derive(Debug, Serialize, JsonSchema)] +pub struct EmptyResponse {} diff --git a/crates/goose-acp/src/lib.rs b/crates/goose-acp/src/lib.rs index e2830d61237b..4772a8e47ad0 100644 --- a/crates/goose-acp/src/lib.rs +++ b/crates/goose-acp/src/lib.rs @@ -1,6 +1,7 @@ #![recursion_limit = "256"] mod adapters; +pub mod custom_requests; pub mod server; pub mod server_factory; pub mod transport; diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 197fcc929cea..6c8282422b47 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -1,3 +1,4 @@ +use crate::custom_requests::*; use anyhow::Result; use fs_err as fs; use goose::agents::extension::{Envs, PLATFORM_EXTENSIONS}; @@ -982,6 +983,237 @@ impl GooseAcpAgent { info!(session_id = %session_id, model_id = %model_id, "Model switched"); Ok(SetSessionModelResponse::new()) } + + /// Handle custom requests with `_`-prefixed method names. + /// + /// These route to the same underlying Agent/SessionManager/Config methods + /// that goose-server's REST handlers use. + async fn handle_custom_request( + &self, + method: &str, + params: serde_json::Value, + ) -> Result { + match method { + "_agent/extensions/add" => self.on_add_extension(params).await, + "_agent/extensions/remove" => self.on_remove_extension(params).await, + "_agent/tools" => self.on_get_tools(params).await, + "_agent/resource/read" => self.on_read_resource(params).await, + "_agent/working_dir/update" => self.on_update_working_dir(params).await, + "_session/list" => self.on_list_sessions().await, + "_session/get" => self.on_get_session(params).await, + "_session/delete" => self.on_delete_session(params).await, + "_session/export" => self.on_export_session(params).await, + "_session/import" => self.on_import_session(params).await, + "_config/extensions" => self.on_get_extensions().await, + // Stubbed — need more complex wiring or types not yet available. + "_agent/tool/call" + | "_agent/provider/update" + | "_agent/container/set" + | "_agent/apps/list" + | "_agent/apps/export" + | "_agent/apps/import" + | "_config/providers" => Err(sacp::Error::new( + -32001, + format!("{method} not yet implemented"), + )), + _ => Err(sacp::Error::method_not_found()), + } + } + + async fn on_add_extension( + &self, + params: serde_json::Value, + ) -> Result { + let req: AddExtensionRequest = Self::parse_params(params)?; + let config: ExtensionConfig = serde_json::from_value(req.config) + .map_err(|e| sacp::Error::invalid_params().data(format!("bad config: {e}")))?; + let agent = self.get_agent_for_session(&req.session_id).await?; + agent + .add_extension(config, &req.session_id) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&EmptyResponse {}) + } + + async fn on_remove_extension( + &self, + params: serde_json::Value, + ) -> Result { + let req: RemoveExtensionRequest = Self::parse_params(params)?; + let agent = self.get_agent_for_session(&req.session_id).await?; + agent + .remove_extension(&req.name, &req.session_id) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&EmptyResponse {}) + } + + async fn on_get_tools( + &self, + params: serde_json::Value, + ) -> Result { + let req: GetToolsRequest = Self::parse_params(params)?; + let agent = self.get_agent_for_session(&req.session_id).await?; + let tools = agent.list_tools(&req.session_id, None).await; + let tools_json = tools + .into_iter() + .map(|t| serde_json::to_value(&t)) + .collect::, _>>() + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&GetToolsResponse { tools: tools_json }) + } + + async fn on_read_resource( + &self, + params: serde_json::Value, + ) -> Result { + let req: ReadResourceRequest = Self::parse_params(params)?; + let agent = self.get_agent_for_session(&req.session_id).await?; + let cancel_token = CancellationToken::new(); + let result = agent + .extension_manager + .read_resource(&req.session_id, &req.uri, &req.extension_name, cancel_token) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let result_json = serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&ReadResourceResponse { + result: result_json, + }) + } + + async fn on_update_working_dir( + &self, + params: serde_json::Value, + ) -> Result { + let req: UpdateWorkingDirRequest = Self::parse_params(params)?; + let working_dir = req.working_dir.trim().to_string(); + if working_dir.is_empty() { + return Err(sacp::Error::invalid_params().data("working directory cannot be empty")); + } + let path = std::path::PathBuf::from(&working_dir); + if !path.exists() || !path.is_dir() { + return Err(sacp::Error::invalid_params().data("invalid directory path")); + } + self.session_manager + .update(&req.session_id) + .working_dir(path) + .apply() + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&EmptyResponse {}) + } + + async fn on_list_sessions(&self) -> Result { + let sessions = self + .session_manager + .list_sessions() + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let sessions_json = sessions + .into_iter() + .map(|s| serde_json::to_value(&s)) + .collect::, _>>() + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&ListSessionsResponse { + sessions: sessions_json, + }) + } + + async fn on_get_session( + &self, + params: serde_json::Value, + ) -> Result { + let req: GetSessionRequest = Self::parse_params(params)?; + let session = self + .session_manager + .get_session(&req.session_id, req.include_messages) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let session_json = serde_json::to_value(&session) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&GetSessionResponse { + session: session_json, + }) + } + + async fn on_delete_session( + &self, + params: serde_json::Value, + ) -> Result { + let req: DeleteSessionRequest = Self::parse_params(params)?; + self.session_manager + .delete_session(&req.session_id) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&EmptyResponse {}) + } + + async fn on_export_session( + &self, + params: serde_json::Value, + ) -> Result { + let req: ExportSessionRequest = Self::parse_params(params)?; + let data = self + .session_manager + .export_session(&req.session_id) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&ExportSessionResponse { data }) + } + + async fn on_import_session( + &self, + params: serde_json::Value, + ) -> Result { + let req: ImportSessionRequest = Self::parse_params(params)?; + let session = self + .session_manager + .import_session(&req.data) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let session_json = serde_json::to_value(&session) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&ImportSessionResponse { + session: session_json, + }) + } + + async fn on_get_extensions(&self) -> Result { + let extensions = goose::config::extensions::get_all_extensions(); + let warnings = goose::config::extensions::get_warnings(); + let extensions_json = extensions + .into_iter() + .map(|e| serde_json::to_value(&e)) + .collect::, _>>() + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Self::to_json(&GetExtensionsResponse { + extensions: extensions_json, + warnings, + }) + } + + fn parse_params( + params: serde_json::Value, + ) -> Result { + serde_json::from_value(params) + .map_err(|e| sacp::Error::invalid_params().data(e.to_string())) + } + + fn to_json(value: &T) -> Result { + serde_json::to_value(value).map_err(|e| sacp::Error::internal_error().data(e.to_string())) + } + + async fn get_agent_for_session(&self, session_id: &str) -> Result, sacp::Error> { + self.sessions + .lock() + .await + .get(session_id) + .map(|s| Arc::clone(&s.agent)) + .ok_or_else(|| { + sacp::Error::invalid_params().data(format!("no active session: {session_id}")) + }) + } } pub struct GooseAcpHandler { @@ -1051,7 +1283,9 @@ impl JrMessageHandler for GooseAcpHandler { self.agent.on_cancel(notif).await }) .await - // HACK: sacp doesn't support session/set_model yet, so we handle it as untyped JSON. + // Handle methods not yet in the sacp typed API. + // - session/set_model: typed support pending in sacp + // - _: custom requests that will eventually route to goose-server .otherwise({ let agent = self.agent.clone(); |message: MessageCx| async move { @@ -1069,6 +1303,13 @@ impl JrMessageHandler for GooseAcpHandler { request_cx.respond(json)?; Ok(()) } + MessageCx::Request(req, request_cx) if req.method.starts_with('_') => { + match agent.handle_custom_request(&req.method, req.params).await { + Ok(json) => request_cx.respond(json)?, + Err(e) => request_cx.respond_with_error(e)?, + } + Ok(()) + } _ => Err(sacp::Error::method_not_found()), } } From c156b26b6256ba634e686cd3aebd44202f5abbd1 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 12:11:11 -0500 Subject: [PATCH 02/16] tests --- .../goose-acp/tests/custom_requests_test.rs | 186 ++++++++++ crates/goose-acp/tests/fixtures/server.rs | 7 + .../integration/acp-custom-requests.test.ts | 344 ++++++++++++++++++ 3 files changed, 537 insertions(+) create mode 100644 crates/goose-acp/tests/custom_requests_test.rs create mode 100644 ui/desktop/tests/integration/acp-custom-requests.test.ts diff --git a/crates/goose-acp/tests/custom_requests_test.rs b/crates/goose-acp/tests/custom_requests_test.rs new file mode 100644 index 000000000000..e378f68e0c40 --- /dev/null +++ b/crates/goose-acp/tests/custom_requests_test.rs @@ -0,0 +1,186 @@ +#[allow(dead_code)] +mod common_tests; + +use common_tests::fixtures::server::ClientToAgentConnection; +use common_tests::fixtures::{run_test, Connection, Session, TestConnectionConfig}; +use goose_test_support::ExpectedSessionId; + +use common_tests::fixtures::OpenAiFixture; + +/// Send an untyped custom request and return the result or error. +async fn send_custom( + cx: &sacp::JrConnectionCx, + method: &str, + params: serde_json::Value, +) -> Result { + let msg = sacp::UntypedMessage::new(method, params).unwrap(); + cx.send_request(msg).block_task().await +} + +#[test] +fn test_custom_session_list() { + run_test(async { + let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; + let mut conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; + + let (session, _models) = conn.new_session().await; + let session_id = session.session_id().0.clone(); + + // Verify the session exists via _session/get + let get_result = send_custom( + conn.cx(), + "_session/get", + serde_json::json!({ "session_id": session_id }), + ) + .await; + assert!( + get_result.is_ok(), + "session should exist via get: {:?}", + get_result + ); + let get_response = get_result.unwrap(); + assert_eq!( + get_response + .get("session") + .and_then(|s| s.get("id")) + .and_then(|v| v.as_str()), + Some(session_id.as_ref()), + ); + + // Verify _session/list returns a valid response + // Note: list_sessions uses INNER JOIN on messages, so a fresh session + // with no messages won't appear. We just verify the call succeeds. + let result = send_custom(conn.cx(), "_session/list", serde_json::json!({})).await; + assert!(result.is_ok(), "expected ok, got: {:?}", result); + let response = result.unwrap(); + let sessions = response.get("sessions").expect("missing 'sessions' field"); + assert!(sessions.is_array(), "sessions should be array"); + }); +} + +#[test] +fn test_custom_session_get() { + run_test(async { + let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; + let mut conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; + + let (session, _models) = conn.new_session().await; + let session_id = session.session_id().0.clone(); + + let result = send_custom( + conn.cx(), + "_session/get", + serde_json::json!({ + "session_id": session_id, + }), + ) + .await; + assert!(result.is_ok(), "expected ok, got: {:?}", result); + + let response = result.unwrap(); + let returned_session = response.get("session").expect("missing 'session' field"); + assert_eq!( + returned_session.get("id").and_then(|v| v.as_str()), + Some(session_id.as_ref()) + ); + }); +} + +#[test] +fn test_custom_session_delete() { + run_test(async { + let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; + let mut conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; + + let (session, _models) = conn.new_session().await; + let session_id = session.session_id().0.clone(); + + let result = send_custom( + conn.cx(), + "_session/delete", + serde_json::json!({ "session_id": session_id }), + ) + .await; + assert!(result.is_ok(), "delete failed: {:?}", result); + + let result = send_custom( + conn.cx(), + "_session/get", + serde_json::json!({ "session_id": session_id }), + ) + .await; + assert!(result.is_err(), "expected error for deleted session"); + }); +} + +#[test] +fn test_custom_get_tools() { + run_test(async { + let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; + let mut conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; + + let (session, _models) = conn.new_session().await; + let session_id = session.session_id().0.clone(); + + let result = send_custom( + conn.cx(), + "_agent/tools", + serde_json::json!({ "session_id": session_id }), + ) + .await; + assert!(result.is_ok(), "expected ok, got: {:?}", result); + + let response = result.unwrap(); + let tools = response.get("tools").expect("missing 'tools' field"); + assert!(tools.is_array(), "tools should be array"); + }); +} + +#[test] +fn test_custom_get_extensions() { + run_test(async { + let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; + let conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; + + let result = send_custom(conn.cx(), "_config/extensions", serde_json::json!({})).await; + assert!(result.is_ok(), "expected ok, got: {:?}", result); + + let response = result.unwrap(); + assert!( + response.get("extensions").is_some(), + "missing 'extensions' field" + ); + assert!( + response.get("warnings").is_some(), + "missing 'warnings' field" + ); + }); +} + +#[test] +fn test_custom_unknown_method() { + run_test(async { + let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; + let conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; + + let result = send_custom(conn.cx(), "_unknown/method", serde_json::json!({})).await; + assert!(result.is_err(), "expected method_not_found error"); + }); +} + +#[test] +fn test_custom_stubbed_method() { + run_test(async { + let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; + let conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; + + let result = send_custom(conn.cx(), "_agent/tool/call", serde_json::json!({})).await; + assert!(result.is_err(), "expected not-yet-implemented error"); + let err = result.unwrap_err(); + assert_eq!( + err.code, + sacp::ErrorCode::Other(-32001), + "expected custom error code -32001" + ); + }); +} diff --git a/crates/goose-acp/tests/fixtures/server.rs b/crates/goose-acp/tests/fixtures/server.rs index 1432e3431da5..1d02eb66251e 100644 --- a/crates/goose-acp/tests/fixtures/server.rs +++ b/crates/goose-acp/tests/fixtures/server.rs @@ -34,6 +34,13 @@ pub struct ClientToAgentSession { notify: Arc, } +impl ClientToAgentConnection { + #[allow(dead_code)] + pub fn cx(&self) -> &JrConnectionCx { + &self.cx + } +} + #[async_trait] impl Connection for ClientToAgentConnection { type Session = ClientToAgentSession; diff --git a/ui/desktop/tests/integration/acp-custom-requests.test.ts b/ui/desktop/tests/integration/acp-custom-requests.test.ts new file mode 100644 index 000000000000..d54c5f05eb8d --- /dev/null +++ b/ui/desktop/tests/integration/acp-custom-requests.test.ts @@ -0,0 +1,344 @@ +/* eslint-disable no-undef */ +/** + * Integration tests for goose-acp-server custom request methods. + * + * Spawns a real goose-acp-server process and sends JSON-RPC requests + * via HTTP+SSE to verify the custom _ handlers work end-to-end. + * + * Tests are split into two groups: + * 1. Session-independent: work with just an ACP session (initialize) + * 2. Session-dependent: require a goose session (session/new) which needs + * a configured provider - these are skipped in environments without one. + */ + +import { spawn, type ChildProcess } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; + +const ACP_SERVER_BINARY = path.resolve(__dirname, '../../../../target/debug/goose-acp-server'); + +interface JsonRpcResponse { + jsonrpc: string; + id?: number; + result?: unknown; + error?: { code: number; message: string; data?: unknown }; +} + +interface AcpTestContext { + baseUrl: string; + serverProcess: ChildProcess; + tempDir: string; + acpSessionId: string; + gooseSessionId: string | null; +} + +let ctx: AcpTestContext; + +/** + * Read an SSE stream from a fetch Response, collecting JSON-RPC messages. + * Resolves once a message with the expected `id` is found (or times out). + */ +async function readSseResponse( + response: Response, + expectedId: number, + timeoutMs = 10000 +): Promise<{ messages: JsonRpcResponse[]; headers: Headers }> { + const messages: JsonRpcResponse[] = []; + const reader = response.body!.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error(`SSE timeout waiting for id=${expectedId}`)), timeoutMs) + ); + + const read = async (): Promise<{ messages: JsonRpcResponse[]; headers: Headers }> => { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('data:')) { + const data = trimmed.slice('data:'.length).trim(); + if (data) { + try { + const parsed = JSON.parse(data) as JsonRpcResponse; + messages.push(parsed); + if (parsed.id === expectedId) { + reader.cancel().catch(() => {}); + return { messages, headers: response.headers }; + } + } catch { + // skip non-JSON data lines + } + } + } + } + } + return { messages, headers: response.headers }; + }; + + return Promise.race([read(), timeout]); +} + +/** + * Send a JSON-RPC request and wait for the matching response via SSE. + */ +async function sendJsonRpc( + baseUrl: string, + method: string, + params: Record, + id: number, + acpSessionId: string +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'Acp-Session-Id': acpSessionId, + }; + + const response = await fetch(`${baseUrl}/acp`, { + method: 'POST', + headers, + body: JSON.stringify({ jsonrpc: '2.0', method, params, id }), + }); + + const { messages } = await readSseResponse(response, id); + const match = messages.find((m) => m.id === id); + if (!match) { + throw new Error(`No response for id=${id}, method=${method}. Got: ${JSON.stringify(messages)}`); + } + return match; +} + +async function waitForServer(baseUrl: string, timeoutMs = 15000): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const resp = await fetch(`${baseUrl}/health`); + if (resp.ok) return; + } catch { + // not ready yet + } + await new Promise((r) => setTimeout(r, 200)); + } + throw new Error(`ACP server did not start within ${timeoutMs}ms`); +} + +async function initializeSession(baseUrl: string): Promise { + const response = await fetch(`${baseUrl}/acp`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + params: { + protocolVersion: '2025-03-26', + clientInfo: { name: 'integration-test', version: '0.1.0' }, + capabilities: {}, + }, + id: 1, + }), + }); + + const acpSessionId = response.headers.get('acp-session-id'); + if (!acpSessionId) { + throw new Error(`No Acp-Session-Id header in initialize response`); + } + + // Consume the SSE stream + await readSseResponse(response, 1); + return acpSessionId; +} + +beforeAll(async () => { + if (!fs.existsSync(ACP_SERVER_BINARY)) { + throw new Error( + `Binary not found at ${ACP_SERVER_BINARY}. Run 'cargo build -p goose-acp' first.` + ); + } + + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'goose-acp-test-')); + const port = 30000 + Math.floor(Math.random() * 10000); + const baseUrl = `http://127.0.0.1:${port}`; + + const serverProcess = spawn( + ACP_SERVER_BINARY, + ['--host', '127.0.0.1', '--port', String(port)], + { + env: { ...process.env, GOOSE_PATH_ROOT: tempDir }, + stdio: ['ignore', 'pipe', 'pipe'], + } + ); + + serverProcess.stderr?.on('data', (data: Buffer) => { + if (process.env.DEBUG) console.error('[acp]', data.toString().trim()); + }); + + ctx = { + baseUrl, + serverProcess, + tempDir, + acpSessionId: '', + gooseSessionId: null, + }; + + console.log(`[ACP TEST] Starting server on port ${port}...`); + await waitForServer(baseUrl); + console.log(`[ACP TEST] Server ready. Initializing ACP session...`); + ctx.acpSessionId = await initializeSession(baseUrl); + console.log(`[ACP TEST] ACP session: ${ctx.acpSessionId}`); +}, 30000); + +afterAll(async () => { + if (ctx?.serverProcess) { + ctx.serverProcess.kill('SIGTERM'); + await new Promise((resolve) => { + ctx.serverProcess.on('close', () => resolve()); + setTimeout(() => { + ctx.serverProcess.kill('SIGKILL'); + resolve(); + }, 5000); + }); + } + if (ctx?.tempDir) { + await fs.promises.rm(ctx.tempDir, { recursive: true, force: true }).catch(() => {}); + } +}); + +describe('ACP custom requests - session independent', () => { + it('_session/list returns a sessions array', async () => { + const response = await sendJsonRpc( + ctx.baseUrl, + '_session/list', + {}, + 10, + ctx.acpSessionId + ); + + expect(response.error).toBeUndefined(); + expect(response.result).toBeDefined(); + + const result = response.result as { sessions: unknown[] }; + expect(Array.isArray(result.sessions)).toBe(true); + }); + + it('_config/extensions returns extensions and warnings', async () => { + const response = await sendJsonRpc( + ctx.baseUrl, + '_config/extensions', + {}, + 11, + ctx.acpSessionId + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { extensions: unknown[]; warnings: unknown[] }; + expect(Array.isArray(result.extensions)).toBe(true); + expect(Array.isArray(result.warnings)).toBe(true); + }); + + it('unknown _ method returns method_not_found error', async () => { + const response = await sendJsonRpc( + ctx.baseUrl, + '_unknown/method', + {}, + 12, + ctx.acpSessionId + ); + + expect(response.error).toBeDefined(); + expect(response.error!.code).toBe(-32601); + }); + + it('stubbed _ method returns not-yet-implemented error', async () => { + const response = await sendJsonRpc( + ctx.baseUrl, + '_agent/tool/call', + {}, + 13, + ctx.acpSessionId + ); + + expect(response.error).toBeDefined(); + expect(response.error!.code).toBe(-32001); + expect(response.error!.message).toContain('not yet implemented'); + }); +}); + +describe('ACP custom requests - session dependent', () => { + it('_session/get retrieves a session', async () => { + if (!ctx.gooseSessionId) { + console.log('Skipping: no goose session (provider not configured)'); + return; + } + + const response = await sendJsonRpc( + ctx.baseUrl, + '_session/get', + { session_id: ctx.gooseSessionId }, + 20, + ctx.acpSessionId + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { session: { id: string } }; + expect(result.session.id).toBe(ctx.gooseSessionId); + }); + + it('_agent/tools returns tools for a session', async () => { + if (!ctx.gooseSessionId) { + console.log('Skipping: no goose session (provider not configured)'); + return; + } + + const response = await sendJsonRpc( + ctx.baseUrl, + '_agent/tools', + { session_id: ctx.gooseSessionId }, + 21, + ctx.acpSessionId + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { tools: unknown[] }; + expect(Array.isArray(result.tools)).toBe(true); + }); + + it('_session/delete removes a session', async () => { + if (!ctx.gooseSessionId) { + console.log('Skipping: no goose session (provider not configured)'); + return; + } + + const deleteResp = await sendJsonRpc( + ctx.baseUrl, + '_session/delete', + { session_id: ctx.gooseSessionId }, + 22, + ctx.acpSessionId + ); + expect(deleteResp.error).toBeUndefined(); + + // Verify it's gone + const getResp = await sendJsonRpc( + ctx.baseUrl, + '_session/get', + { session_id: ctx.gooseSessionId }, + 23, + ctx.acpSessionId + ); + expect(getResp.error).toBeDefined(); + }); +}); From 6fcac8ca65b2ba5cfe78b8a5841774586f01c904 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 12:14:40 -0500 Subject: [PATCH 03/16] format --- .../integration/acp-custom-requests.test.ts | 44 ++++--------------- 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/ui/desktop/tests/integration/acp-custom-requests.test.ts b/ui/desktop/tests/integration/acp-custom-requests.test.ts index d54c5f05eb8d..754723d0c0c6 100644 --- a/ui/desktop/tests/integration/acp-custom-requests.test.ts +++ b/ui/desktop/tests/integration/acp-custom-requests.test.ts @@ -173,14 +173,10 @@ beforeAll(async () => { const port = 30000 + Math.floor(Math.random() * 10000); const baseUrl = `http://127.0.0.1:${port}`; - const serverProcess = spawn( - ACP_SERVER_BINARY, - ['--host', '127.0.0.1', '--port', String(port)], - { - env: { ...process.env, GOOSE_PATH_ROOT: tempDir }, - stdio: ['ignore', 'pipe', 'pipe'], - } - ); + const serverProcess = spawn(ACP_SERVER_BINARY, ['--host', '127.0.0.1', '--port', String(port)], { + env: { ...process.env, GOOSE_PATH_ROOT: tempDir }, + stdio: ['ignore', 'pipe', 'pipe'], + }); serverProcess.stderr?.on('data', (data: Buffer) => { if (process.env.DEBUG) console.error('[acp]', data.toString().trim()); @@ -219,13 +215,7 @@ afterAll(async () => { describe('ACP custom requests - session independent', () => { it('_session/list returns a sessions array', async () => { - const response = await sendJsonRpc( - ctx.baseUrl, - '_session/list', - {}, - 10, - ctx.acpSessionId - ); + const response = await sendJsonRpc(ctx.baseUrl, '_session/list', {}, 10, ctx.acpSessionId); expect(response.error).toBeUndefined(); expect(response.result).toBeDefined(); @@ -235,13 +225,7 @@ describe('ACP custom requests - session independent', () => { }); it('_config/extensions returns extensions and warnings', async () => { - const response = await sendJsonRpc( - ctx.baseUrl, - '_config/extensions', - {}, - 11, - ctx.acpSessionId - ); + const response = await sendJsonRpc(ctx.baseUrl, '_config/extensions', {}, 11, ctx.acpSessionId); expect(response.error).toBeUndefined(); const result = response.result as { extensions: unknown[]; warnings: unknown[] }; @@ -250,26 +234,14 @@ describe('ACP custom requests - session independent', () => { }); it('unknown _ method returns method_not_found error', async () => { - const response = await sendJsonRpc( - ctx.baseUrl, - '_unknown/method', - {}, - 12, - ctx.acpSessionId - ); + const response = await sendJsonRpc(ctx.baseUrl, '_unknown/method', {}, 12, ctx.acpSessionId); expect(response.error).toBeDefined(); expect(response.error!.code).toBe(-32601); }); it('stubbed _ method returns not-yet-implemented error', async () => { - const response = await sendJsonRpc( - ctx.baseUrl, - '_agent/tool/call', - {}, - 13, - ctx.acpSessionId - ); + const response = await sendJsonRpc(ctx.baseUrl, '_agent/tool/call', {}, 13, ctx.acpSessionId); expect(response.error).toBeDefined(); expect(response.error!.code).toBe(-32001); From 9f5f11ed0407d269282bd83e25ed14b6833537e2 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 12:23:00 -0500 Subject: [PATCH 04/16] _goose prefix --- crates/goose-acp/src/server.rs | 36 +++++++++---------- .../goose-acp/tests/custom_requests_test.rs | 17 ++++----- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 6c8282422b47..04dd5f40769e 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -994,25 +994,25 @@ impl GooseAcpAgent { params: serde_json::Value, ) -> Result { match method { - "_agent/extensions/add" => self.on_add_extension(params).await, - "_agent/extensions/remove" => self.on_remove_extension(params).await, - "_agent/tools" => self.on_get_tools(params).await, - "_agent/resource/read" => self.on_read_resource(params).await, - "_agent/working_dir/update" => self.on_update_working_dir(params).await, - "_session/list" => self.on_list_sessions().await, - "_session/get" => self.on_get_session(params).await, - "_session/delete" => self.on_delete_session(params).await, - "_session/export" => self.on_export_session(params).await, - "_session/import" => self.on_import_session(params).await, - "_config/extensions" => self.on_get_extensions().await, + "_goose/extensions/add" => self.on_add_extension(params).await, + "_goose/extensions/remove" => self.on_remove_extension(params).await, + "_goose/tools" => self.on_get_tools(params).await, + "_goose/resource/read" => self.on_read_resource(params).await, + "_goose/working_dir/update" => self.on_update_working_dir(params).await, + "_goose/session/list" => self.on_list_sessions().await, + "_goose/session/get" => self.on_get_session(params).await, + "_goose/session/delete" => self.on_delete_session(params).await, + "_goose/session/export" => self.on_export_session(params).await, + "_goose/session/import" => self.on_import_session(params).await, + "_goose/config/extensions" => self.on_get_extensions().await, // Stubbed — need more complex wiring or types not yet available. - "_agent/tool/call" - | "_agent/provider/update" - | "_agent/container/set" - | "_agent/apps/list" - | "_agent/apps/export" - | "_agent/apps/import" - | "_config/providers" => Err(sacp::Error::new( + "_goose/tool/call" + | "_goose/provider/update" + | "_goose/container/set" + | "_goose/apps/list" + | "_goose/apps/export" + | "_goose/apps/import" + | "_goose/config/providers" => Err(sacp::Error::new( -32001, format!("{method} not yet implemented"), )), diff --git a/crates/goose-acp/tests/custom_requests_test.rs b/crates/goose-acp/tests/custom_requests_test.rs index e378f68e0c40..862eaec99572 100644 --- a/crates/goose-acp/tests/custom_requests_test.rs +++ b/crates/goose-acp/tests/custom_requests_test.rs @@ -29,7 +29,7 @@ fn test_custom_session_list() { // Verify the session exists via _session/get let get_result = send_custom( conn.cx(), - "_session/get", + "_goose/session/get", serde_json::json!({ "session_id": session_id }), ) .await; @@ -50,7 +50,7 @@ fn test_custom_session_list() { // Verify _session/list returns a valid response // Note: list_sessions uses INNER JOIN on messages, so a fresh session // with no messages won't appear. We just verify the call succeeds. - let result = send_custom(conn.cx(), "_session/list", serde_json::json!({})).await; + let result = send_custom(conn.cx(), "_goose/session/list", serde_json::json!({})).await; assert!(result.is_ok(), "expected ok, got: {:?}", result); let response = result.unwrap(); let sessions = response.get("sessions").expect("missing 'sessions' field"); @@ -69,7 +69,7 @@ fn test_custom_session_get() { let result = send_custom( conn.cx(), - "_session/get", + "_goose/session/get", serde_json::json!({ "session_id": session_id, }), @@ -97,7 +97,7 @@ fn test_custom_session_delete() { let result = send_custom( conn.cx(), - "_session/delete", + "_goose/session/delete", serde_json::json!({ "session_id": session_id }), ) .await; @@ -105,7 +105,7 @@ fn test_custom_session_delete() { let result = send_custom( conn.cx(), - "_session/get", + "_goose/session/get", serde_json::json!({ "session_id": session_id }), ) .await; @@ -124,7 +124,7 @@ fn test_custom_get_tools() { let result = send_custom( conn.cx(), - "_agent/tools", + "_goose/tools", serde_json::json!({ "session_id": session_id }), ) .await; @@ -142,7 +142,8 @@ fn test_custom_get_extensions() { let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; let conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; - let result = send_custom(conn.cx(), "_config/extensions", serde_json::json!({})).await; + let result = + send_custom(conn.cx(), "_goose/config/extensions", serde_json::json!({})).await; assert!(result.is_ok(), "expected ok, got: {:?}", result); let response = result.unwrap(); @@ -174,7 +175,7 @@ fn test_custom_stubbed_method() { let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; let conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; - let result = send_custom(conn.cx(), "_agent/tool/call", serde_json::json!({})).await; + let result = send_custom(conn.cx(), "_goose/tool/call", serde_json::json!({})).await; assert!(result.is_err(), "expected not-yet-implemented error"); let err = result.unwrap_err(); assert_eq!( From fb3821efd1f7bd6e2e80aa56c3e11198d51e4c43 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 12:36:09 -0500 Subject: [PATCH 05/16] Macros --- Cargo.lock | 10 ++ crates/goose-acp-macros/Cargo.toml | 19 +++ crates/goose-acp-macros/src/lib.rs | 154 ++++++++++++++++++++++++ crates/goose-acp/Cargo.toml | 1 + crates/goose-acp/src/server.rs | 185 ++++++++++++++++------------- 5 files changed, 284 insertions(+), 85 deletions(-) create mode 100644 crates/goose-acp-macros/Cargo.toml create mode 100644 crates/goose-acp-macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 886878d4f460..7ed4711a34c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4279,6 +4279,7 @@ dependencies = [ "fs-err", "futures", "goose", + "goose-acp-macros", "goose-mcp", "goose-test-support", "http-body-util", @@ -4300,6 +4301,15 @@ dependencies = [ "wiremock", ] +[[package]] +name = "goose-acp-macros" +version = "1.24.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "goose-cli" version = "1.24.0" diff --git a/crates/goose-acp-macros/Cargo.toml b/crates/goose-acp-macros/Cargo.toml new file mode 100644 index 000000000000..eeda799a4953 --- /dev/null +++ b/crates/goose-acp-macros/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "goose-acp-macros" +edition.workspace = true +version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full", "extra-traits"] } + +[lints] +workspace = true diff --git a/crates/goose-acp-macros/src/lib.rs b/crates/goose-acp-macros/src/lib.rs new file mode 100644 index 000000000000..a35f7fd69cba --- /dev/null +++ b/crates/goose-acp-macros/src/lib.rs @@ -0,0 +1,154 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Lit, Pat, ReturnType, Type}; + +/// Marks an impl block as containing `#[custom_method("...")]`-annotated handlers. +/// +/// Generates a `handle_custom_request` dispatcher that: +/// - Prefixes each method name with `_goose/` +/// - Parses JSON params into the handler's typed parameter (if any) +/// - Serializes the handler's return value to JSON +/// +/// # Handler signatures +/// +/// Handlers may take zero or one parameter (beyond `&self`): +/// +/// ```ignore +/// // No params — called for requests with no/empty params +/// #[custom_method("session/list")] +/// async fn on_list_sessions(&self) -> Result { .. } +/// +/// // Typed params — JSON params auto-deserialized +/// #[custom_method("session/get")] +/// async fn on_get_session(&self, req: GetSessionRequest) -> Result { .. } +/// ``` +/// +/// The return type must be `Result` where `T: Serialize`. +#[proc_macro_attribute] +pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut impl_block = parse_macro_input!(item as ItemImpl); + + let mut routes: Vec = Vec::new(); + + // Collect all #[custom_method("...")] annotations and strip them. + for item in &mut impl_block.items { + if let ImplItem::Fn(method) = item { + let mut route_name = None; + method.attrs.retain(|attr| { + if attr.path().is_ident("custom_method") { + if let Ok(meta_list) = attr.meta.require_list() { + if let Ok(Lit::Str(s)) = meta_list.parse_args::() { + route_name = Some(s.value()); + } + } + false // strip the attribute + } else { + true // keep other attributes + } + }); + + if let Some(name) = route_name { + let fn_ident = method.sig.ident.clone(); + + // Determine if the method takes a typed parameter (beyond &self). + let param_type = extract_param_type(&method.sig); + let return_type = extract_return_type(&method.sig); + + routes.push(Route { + method_name: name, + fn_ident, + param_type, + return_type, + }); + } + } + } + + // Generate the dispatch arms. + let arms: Vec<_> = routes + .iter() + .map(|route| { + let full_method = format!("_goose/{}", route.method_name); + let fn_ident = &route.fn_ident; + + match &route.param_type { + Some(_) => { + // Handler takes a typed param: parse from JSON, call, serialize result. + quote! { + #full_method => { + let req = serde_json::from_value(params) + .map_err(|e| sacp::Error::invalid_params().data(e.to_string()))?; + let result = self.#fn_ident(req).await?; + serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string())) + } + } + } + None => { + // Handler takes no params: call directly, serialize result. + quote! { + #full_method => { + let result = self.#fn_ident().await?; + serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string())) + } + } + } + } + }) + .collect(); + + // Generate the handle_custom_request method. + let dispatcher = quote! { + async fn handle_custom_request( + &self, + method: &str, + params: serde_json::Value, + ) -> Result { + match method { + #(#arms)* + _ => Err(sacp::Error::method_not_found()), + } + } + }; + + // Append the generated dispatcher to the impl block. + let dispatcher_item: ImplItem = + syn::parse2(dispatcher).expect("generated dispatcher must parse"); + impl_block.items.push(dispatcher_item); + + TokenStream::from(quote! { #impl_block }) +} + +struct Route { + method_name: String, + fn_ident: syn::Ident, + param_type: Option, + #[allow(dead_code)] + return_type: Option, +} + +/// Extract the type of the first non-self parameter, if any. +fn extract_param_type(sig: &syn::Signature) -> Option { + for input in &sig.inputs { + if let FnArg::Typed(pat_type) = input { + // Skip if it's self + if let Pat::Ident(pat_ident) = &*pat_type.pat { + if pat_ident.ident == "self" { + continue; + } + } + return Some((*pat_type.ty).clone()); + } + } + None +} + +/// Extract the Ok type from Result, if the return type is a Result. +fn extract_return_type(sig: &syn::Signature) -> Option { + if let ReturnType::Type(_, ty) = &sig.output { + Some((**ty).clone()) + } else { + None + } +} diff --git a/crates/goose-acp/Cargo.toml b/crates/goose-acp/Cargo.toml index 16f2569c51ae..f92395116bdd 100644 --- a/crates/goose-acp/Cargo.toml +++ b/crates/goose-acp/Cargo.toml @@ -41,6 +41,7 @@ bytes = { workspace = true } http-body-util = "0.1.3" uuid = { workspace = true, features = ["v7"] } schemars = { workspace = true, features = ["derive"] } +goose-acp-macros = { version = "1.24.0", path = "../goose-acp-macros" } [dev-dependencies] assert-json-diff = "2.0.2" diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 04dd5f40769e..3ebeddd7a3a0 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -18,6 +18,7 @@ use goose::providers::base::Provider; use goose::providers::provider_registry::ProviderConstructor; use goose::session::session_manager::SessionType; use goose::session::{Session, SessionManager}; +use goose_acp_macros::custom_methods; use rmcp::model::{CallToolResult, RawContent, ResourceContents, Role}; use sacp::schema::{ AgentCapabilities, AuthMethod, AuthenticateRequest, AuthenticateResponse, BlobResourceContents, @@ -983,48 +984,15 @@ impl GooseAcpAgent { info!(session_id = %session_id, model_id = %model_id, "Model switched"); Ok(SetSessionModelResponse::new()) } +} - /// Handle custom requests with `_`-prefixed method names. - /// - /// These route to the same underlying Agent/SessionManager/Config methods - /// that goose-server's REST handlers use. - async fn handle_custom_request( - &self, - method: &str, - params: serde_json::Value, - ) -> Result { - match method { - "_goose/extensions/add" => self.on_add_extension(params).await, - "_goose/extensions/remove" => self.on_remove_extension(params).await, - "_goose/tools" => self.on_get_tools(params).await, - "_goose/resource/read" => self.on_read_resource(params).await, - "_goose/working_dir/update" => self.on_update_working_dir(params).await, - "_goose/session/list" => self.on_list_sessions().await, - "_goose/session/get" => self.on_get_session(params).await, - "_goose/session/delete" => self.on_delete_session(params).await, - "_goose/session/export" => self.on_export_session(params).await, - "_goose/session/import" => self.on_import_session(params).await, - "_goose/config/extensions" => self.on_get_extensions().await, - // Stubbed — need more complex wiring or types not yet available. - "_goose/tool/call" - | "_goose/provider/update" - | "_goose/container/set" - | "_goose/apps/list" - | "_goose/apps/export" - | "_goose/apps/import" - | "_goose/config/providers" => Err(sacp::Error::new( - -32001, - format!("{method} not yet implemented"), - )), - _ => Err(sacp::Error::method_not_found()), - } - } - +#[custom_methods] +impl GooseAcpAgent { + #[custom_method("extensions/add")] async fn on_add_extension( &self, - params: serde_json::Value, - ) -> Result { - let req: AddExtensionRequest = Self::parse_params(params)?; + req: AddExtensionRequest, + ) -> Result { let config: ExtensionConfig = serde_json::from_value(req.config) .map_err(|e| sacp::Error::invalid_params().data(format!("bad config: {e}")))?; let agent = self.get_agent_for_session(&req.session_id).await?; @@ -1032,27 +1000,24 @@ impl GooseAcpAgent { .add_extension(config, &req.session_id) .await .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&EmptyResponse {}) + Ok(EmptyResponse {}) } + #[custom_method("extensions/remove")] async fn on_remove_extension( &self, - params: serde_json::Value, - ) -> Result { - let req: RemoveExtensionRequest = Self::parse_params(params)?; + req: RemoveExtensionRequest, + ) -> Result { let agent = self.get_agent_for_session(&req.session_id).await?; agent .remove_extension(&req.name, &req.session_id) .await .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&EmptyResponse {}) + Ok(EmptyResponse {}) } - async fn on_get_tools( - &self, - params: serde_json::Value, - ) -> Result { - let req: GetToolsRequest = Self::parse_params(params)?; + #[custom_method("tools")] + async fn on_get_tools(&self, req: GetToolsRequest) -> Result { let agent = self.get_agent_for_session(&req.session_id).await?; let tools = agent.list_tools(&req.session_id, None).await; let tools_json = tools @@ -1060,14 +1025,14 @@ impl GooseAcpAgent { .map(|t| serde_json::to_value(&t)) .collect::, _>>() .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&GetToolsResponse { tools: tools_json }) + Ok(GetToolsResponse { tools: tools_json }) } + #[custom_method("resource/read")] async fn on_read_resource( &self, - params: serde_json::Value, - ) -> Result { - let req: ReadResourceRequest = Self::parse_params(params)?; + req: ReadResourceRequest, + ) -> Result { let agent = self.get_agent_for_session(&req.session_id).await?; let cancel_token = CancellationToken::new(); let result = agent @@ -1077,16 +1042,16 @@ impl GooseAcpAgent { .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; let result_json = serde_json::to_value(&result) .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&ReadResourceResponse { + Ok(ReadResourceResponse { result: result_json, }) } + #[custom_method("working_dir/update")] async fn on_update_working_dir( &self, - params: serde_json::Value, - ) -> Result { - let req: UpdateWorkingDirRequest = Self::parse_params(params)?; + req: UpdateWorkingDirRequest, + ) -> Result { let working_dir = req.working_dir.trim().to_string(); if working_dir.is_empty() { return Err(sacp::Error::invalid_params().data("working directory cannot be empty")); @@ -1101,10 +1066,11 @@ impl GooseAcpAgent { .apply() .await .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&EmptyResponse {}) + Ok(EmptyResponse {}) } - async fn on_list_sessions(&self) -> Result { + #[custom_method("session/list")] + async fn on_list_sessions(&self) -> Result { let sessions = self .session_manager .list_sessions() @@ -1115,16 +1081,16 @@ impl GooseAcpAgent { .map(|s| serde_json::to_value(&s)) .collect::, _>>() .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&ListSessionsResponse { + Ok(ListSessionsResponse { sessions: sessions_json, }) } + #[custom_method("session/get")] async fn on_get_session( &self, - params: serde_json::Value, - ) -> Result { - let req: GetSessionRequest = Self::parse_params(params)?; + req: GetSessionRequest, + ) -> Result { let session = self .session_manager .get_session(&req.session_id, req.include_messages) @@ -1132,41 +1098,41 @@ impl GooseAcpAgent { .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; let session_json = serde_json::to_value(&session) .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&GetSessionResponse { + Ok(GetSessionResponse { session: session_json, }) } + #[custom_method("session/delete")] async fn on_delete_session( &self, - params: serde_json::Value, - ) -> Result { - let req: DeleteSessionRequest = Self::parse_params(params)?; + req: DeleteSessionRequest, + ) -> Result { self.session_manager .delete_session(&req.session_id) .await .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&EmptyResponse {}) + Ok(EmptyResponse {}) } + #[custom_method("session/export")] async fn on_export_session( &self, - params: serde_json::Value, - ) -> Result { - let req: ExportSessionRequest = Self::parse_params(params)?; + req: ExportSessionRequest, + ) -> Result { let data = self .session_manager .export_session(&req.session_id) .await .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&ExportSessionResponse { data }) + Ok(ExportSessionResponse { data }) } + #[custom_method("session/import")] async fn on_import_session( &self, - params: serde_json::Value, - ) -> Result { - let req: ImportSessionRequest = Self::parse_params(params)?; + req: ImportSessionRequest, + ) -> Result { let session = self .session_manager .import_session(&req.data) @@ -1174,12 +1140,13 @@ impl GooseAcpAgent { .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; let session_json = serde_json::to_value(&session) .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&ImportSessionResponse { + Ok(ImportSessionResponse { session: session_json, }) } - async fn on_get_extensions(&self) -> Result { + #[custom_method("config/extensions")] + async fn on_get_extensions(&self) -> Result { let extensions = goose::config::extensions::get_all_extensions(); let warnings = goose::config::extensions::get_warnings(); let extensions_json = extensions @@ -1187,21 +1154,69 @@ impl GooseAcpAgent { .map(|e| serde_json::to_value(&e)) .collect::, _>>() .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; - Self::to_json(&GetExtensionsResponse { + Ok(GetExtensionsResponse { extensions: extensions_json, warnings, }) } - fn parse_params( - params: serde_json::Value, - ) -> Result { - serde_json::from_value(params) - .map_err(|e| sacp::Error::invalid_params().data(e.to_string())) + #[custom_method("tool/call")] + async fn on_tool_call( + &self, + _req: serde_json::Value, + ) -> Result { + Err(sacp::Error::new(-32001, "tool/call not yet implemented")) } - fn to_json(value: &T) -> Result { - serde_json::to_value(value).map_err(|e| sacp::Error::internal_error().data(e.to_string())) + #[custom_method("provider/update")] + async fn on_provider_update( + &self, + _req: serde_json::Value, + ) -> Result { + Err(sacp::Error::new( + -32001, + "provider/update not yet implemented", + )) + } + + #[custom_method("container/set")] + async fn on_container_set( + &self, + _req: serde_json::Value, + ) -> Result { + Err(sacp::Error::new( + -32001, + "container/set not yet implemented", + )) + } + + #[custom_method("apps/list")] + async fn on_apps_list(&self) -> Result { + Err(sacp::Error::new(-32001, "apps/list not yet implemented")) + } + + #[custom_method("apps/export")] + async fn on_apps_export( + &self, + _req: serde_json::Value, + ) -> Result { + Err(sacp::Error::new(-32001, "apps/export not yet implemented")) + } + + #[custom_method("apps/import")] + async fn on_apps_import( + &self, + _req: serde_json::Value, + ) -> Result { + Err(sacp::Error::new(-32001, "apps/import not yet implemented")) + } + + #[custom_method("config/providers")] + async fn on_config_providers(&self) -> Result { + Err(sacp::Error::new( + -32001, + "config/providers not yet implemented", + )) } async fn get_agent_for_session(&self, session_id: &str) -> Result, sacp::Error> { From d24de4676b831afc77b9d865e9c7c05ececade16 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 12:36:15 -0500 Subject: [PATCH 06/16] test update --- .../integration/acp-custom-requests.test.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/ui/desktop/tests/integration/acp-custom-requests.test.ts b/ui/desktop/tests/integration/acp-custom-requests.test.ts index 754723d0c0c6..2cc2242f6249 100644 --- a/ui/desktop/tests/integration/acp-custom-requests.test.ts +++ b/ui/desktop/tests/integration/acp-custom-requests.test.ts @@ -214,8 +214,14 @@ afterAll(async () => { }); describe('ACP custom requests - session independent', () => { - it('_session/list returns a sessions array', async () => { - const response = await sendJsonRpc(ctx.baseUrl, '_session/list', {}, 10, ctx.acpSessionId); + it('session/list returns a sessions array', async () => { + const response = await sendJsonRpc( + ctx.baseUrl, + '_goose/session/list', + {}, + 10, + ctx.acpSessionId + ); expect(response.error).toBeUndefined(); expect(response.result).toBeDefined(); @@ -224,8 +230,14 @@ describe('ACP custom requests - session independent', () => { expect(Array.isArray(result.sessions)).toBe(true); }); - it('_config/extensions returns extensions and warnings', async () => { - const response = await sendJsonRpc(ctx.baseUrl, '_config/extensions', {}, 11, ctx.acpSessionId); + it('config/extensions returns extensions and warnings', async () => { + const response = await sendJsonRpc( + ctx.baseUrl, + '_goose/config/extensions', + {}, + 11, + ctx.acpSessionId + ); expect(response.error).toBeUndefined(); const result = response.result as { extensions: unknown[]; warnings: unknown[] }; @@ -239,14 +251,6 @@ describe('ACP custom requests - session independent', () => { expect(response.error).toBeDefined(); expect(response.error!.code).toBe(-32601); }); - - it('stubbed _ method returns not-yet-implemented error', async () => { - const response = await sendJsonRpc(ctx.baseUrl, '_agent/tool/call', {}, 13, ctx.acpSessionId); - - expect(response.error).toBeDefined(); - expect(response.error!.code).toBe(-32001); - expect(response.error!.message).toContain('not yet implemented'); - }); }); describe('ACP custom requests - session dependent', () => { From 87645e3a243eab89de32c88d8eb35d60a85bae04 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 13:00:31 -0500 Subject: [PATCH 07/16] JSON schema generation --- crates/goose-acp-macros/src/lib.rs | 117 ++++++++++++++++-- crates/goose-acp/Cargo.toml | 4 + .../goose-acp/src/bin/generate_acp_schema.rs | 20 +++ crates/goose-acp/src/custom_requests.rs | 13 ++ 4 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 crates/goose-acp/src/bin/generate_acp_schema.rs diff --git a/crates/goose-acp-macros/src/lib.rs b/crates/goose-acp-macros/src/lib.rs index a35f7fd69cba..296db728797b 100644 --- a/crates/goose-acp-macros/src/lib.rs +++ b/crates/goose-acp-macros/src/lib.rs @@ -1,13 +1,23 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Lit, Pat, ReturnType, Type}; +use syn::{ + parse_macro_input, FnArg, GenericArgument, ImplItem, ItemImpl, Lit, Pat, PathArguments, + ReturnType, Type, +}; /// Marks an impl block as containing `#[custom_method("...")]`-annotated handlers. /// -/// Generates a `handle_custom_request` dispatcher that: -/// - Prefixes each method name with `_goose/` -/// - Parses JSON params into the handler's typed parameter (if any) -/// - Serializes the handler's return value to JSON +/// Generates two methods on the impl: +/// +/// 1. `handle_custom_request` — a dispatcher that: +/// - Prefixes each method name with `_goose/` +/// - Parses JSON params into the handler's typed parameter (if any) +/// - Serializes the handler's return value to JSON +/// +/// 2. `custom_method_schemas` — returns a `Vec` with +/// JSON Schema for each method's params and response types. Types that +/// implement `schemars::JsonSchema` get a full schema; `serde_json::Value` +/// params/responses produce `None`. /// /// # Handler signatures /// @@ -50,15 +60,16 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { if let Some(name) = route_name { let fn_ident = method.sig.ident.clone(); - // Determine if the method takes a typed parameter (beyond &self). let param_type = extract_param_type(&method.sig); let return_type = extract_return_type(&method.sig); + let ok_type = extract_result_ok_type(&method.sig); routes.push(Route { method_name: name, fn_ident, param_type, return_type, + ok_type, }); } } @@ -73,7 +84,6 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { match &route.param_type { Some(_) => { - // Handler takes a typed param: parse from JSON, call, serialize result. quote! { #full_method => { let req = serde_json::from_value(params) @@ -85,7 +95,6 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { } } None => { - // Handler takes no params: call directly, serialize result. quote! { #full_method => { let result = self.#fn_ident().await?; @@ -98,6 +107,42 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { }) .collect(); + // Generate schema entries for each route. + let schema_entries: Vec<_> = routes + .iter() + .map(|route| { + let full_method = format!("_goose/{}", route.method_name); + + let params_expr = if let Some(pt) = &route.param_type { + if is_json_value(pt) { + quote! { None } + } else { + quote! { Some(schemars::schema_for!(#pt)) } + } + } else { + quote! { None } + }; + + let response_expr = if let Some(ok_ty) = &route.ok_type { + if is_json_value(ok_ty) { + quote! { None } + } else { + quote! { Some(schemars::schema_for!(#ok_ty)) } + } + } else { + quote! { None } + }; + + quote! { + crate::custom_requests::CustomMethodSchema { + method: #full_method.to_string(), + params_schema: #params_expr, + response_schema: #response_expr, + } + } + }) + .collect(); + // Generate the handle_custom_request method. let dispatcher = quote! { async fn handle_custom_request( @@ -112,11 +157,23 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { } }; - // Append the generated dispatcher to the impl block. + // Generate the custom_method_schemas method. + let schemas_fn = quote! { + pub fn custom_method_schemas() -> Vec { + vec![ + #(#schema_entries),* + ] + } + }; + + // Append the generated methods to the impl block. let dispatcher_item: ImplItem = syn::parse2(dispatcher).expect("generated dispatcher must parse"); impl_block.items.push(dispatcher_item); + let schemas_item: ImplItem = syn::parse2(schemas_fn).expect("generated schemas fn must parse"); + impl_block.items.push(schemas_item); + TokenStream::from(quote! { #impl_block }) } @@ -126,13 +183,13 @@ struct Route { param_type: Option, #[allow(dead_code)] return_type: Option, + ok_type: Option, } /// Extract the type of the first non-self parameter, if any. fn extract_param_type(sig: &syn::Signature) -> Option { for input in &sig.inputs { if let FnArg::Typed(pat_type) = input { - // Skip if it's self if let Pat::Ident(pat_ident) = &*pat_type.pat { if pat_ident.ident == "self" { continue; @@ -144,7 +201,7 @@ fn extract_param_type(sig: &syn::Signature) -> Option { None } -/// Extract the Ok type from Result, if the return type is a Result. +/// Extract the full return type (e.g. `Result`). fn extract_return_type(sig: &syn::Signature) -> Option { if let ReturnType::Type(_, ty) = &sig.output { Some((**ty).clone()) @@ -152,3 +209,41 @@ fn extract_return_type(sig: &syn::Signature) -> Option { None } } + +/// Extract `T` from `Result` in the return type. +fn extract_result_ok_type(sig: &syn::Signature) -> Option { + let ty = match &sig.output { + ReturnType::Type(_, ty) => ty, + _ => return None, + }; + + // Peel through the type to find a path ending in `Result`. + if let Type::Path(type_path) = ty.as_ref() { + let last_seg = type_path.path.segments.last()?; + if last_seg.ident == "Result" { + if let PathArguments::AngleBracketed(args) = &last_seg.arguments { + // First generic argument is the Ok type. + if let Some(GenericArgument::Type(ok_ty)) = args.args.first() { + return Some(ok_ty.clone()); + } + } + } + } + None +} + +/// Check if a type is `serde_json::Value` (matches `Value` or `serde_json::Value`). +fn is_json_value(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + let segments: Vec<_> = type_path + .path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect(); + let strs: Vec<&str> = segments.iter().map(|s| s.as_str()).collect(); + matches!(strs.as_slice(), ["serde_json", "Value"] | ["Value"]) + } else { + false + } +} diff --git a/crates/goose-acp/Cargo.toml b/crates/goose-acp/Cargo.toml index f92395116bdd..69be0e5af957 100644 --- a/crates/goose-acp/Cargo.toml +++ b/crates/goose-acp/Cargo.toml @@ -11,6 +11,10 @@ description.workspace = true name = "goose-acp-server" path = "src/bin/server.rs" +[[bin]] +name = "generate-acp-schema" +path = "src/bin/generate_acp_schema.rs" + [lints] workspace = true diff --git a/crates/goose-acp/src/bin/generate_acp_schema.rs b/crates/goose-acp/src/bin/generate_acp_schema.rs new file mode 100644 index 000000000000..d3931a8b6fc6 --- /dev/null +++ b/crates/goose-acp/src/bin/generate_acp_schema.rs @@ -0,0 +1,20 @@ +use goose_acp::server::GooseAcpAgent; +use std::env; +use std::fs; +use std::path::PathBuf; + +fn main() { + let schemas = GooseAcpAgent::custom_method_schemas(); + let json = serde_json::to_string_pretty(&schemas).expect("failed to serialize schemas"); + + let package_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let output_path = PathBuf::from(package_dir).join("acp-schema.json"); + + fs::write(&output_path, format!("{json}\n")).expect("failed to write schema file"); + eprintln!( + "Generated ACP custom method schemas at {}", + output_path.display() + ); + + println!("{json}"); +} diff --git a/crates/goose-acp/src/custom_requests.rs b/crates/goose-acp/src/custom_requests.rs index e8a10c3e22f2..587776f9618b 100644 --- a/crates/goose-acp/src/custom_requests.rs +++ b/crates/goose-acp/src/custom_requests.rs @@ -1,6 +1,19 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +// --------------------------------------------------------------------------- +// Schema metadata for custom methods +// --------------------------------------------------------------------------- + +/// Schema descriptor for a single custom method, produced by the +/// `#[custom_methods]` macro's generated `custom_method_schemas()` function. +#[derive(Debug, Serialize)] +pub struct CustomMethodSchema { + pub method: String, + pub params_schema: Option, + pub response_schema: Option, +} + // --------------------------------------------------------------------------- // Agent: extensions // --------------------------------------------------------------------------- From ac97276b1f0ac47f29c834ba087ed03e42f87969 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 14:39:22 -0500 Subject: [PATCH 08/16] TS schema --- crates/goose-acp-macros/src/lib.rs | 43 +- crates/goose-acp/acp-meta.json | 94 ++++ crates/goose-acp/acp-schema.json | 520 ++++++++++++++++++ .../goose-acp/src/bin/generate_acp_schema.rs | 186 ++++++- crates/goose-acp/src/custom_requests.rs | 9 + ui/desktop/generate-goose-ext-schema.ts | 114 ++++ ui/desktop/package-lock.json | 42 +- ui/desktop/package.json | 3 +- ui/desktop/src/goose-ext-schema/index.ts | 118 ++++ ui/desktop/src/goose-ext-schema/types.gen.ts | 186 +++++++ ui/desktop/src/goose-ext-schema/zod.gen.ts | 176 ++++++ 11 files changed, 1445 insertions(+), 46 deletions(-) create mode 100644 crates/goose-acp/acp-meta.json create mode 100644 crates/goose-acp/acp-schema.json create mode 100644 ui/desktop/generate-goose-ext-schema.ts create mode 100644 ui/desktop/src/goose-ext-schema/index.ts create mode 100644 ui/desktop/src/goose-ext-schema/types.gen.ts create mode 100644 ui/desktop/src/goose-ext-schema/zod.gen.ts diff --git a/crates/goose-acp-macros/src/lib.rs b/crates/goose-acp-macros/src/lib.rs index 296db728797b..48fa06f99e51 100644 --- a/crates/goose-acp-macros/src/lib.rs +++ b/crates/goose-acp-macros/src/lib.rs @@ -107,7 +107,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { }) .collect(); - // Generate schema entries for each route. + // Generate schema entries for each route using SchemaGenerator for $ref dedup. let schema_entries: Vec<_> = routes .iter() .map(|route| { @@ -117,7 +117,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { if is_json_value(pt) { quote! { None } } else { - quote! { Some(schemars::schema_for!(#pt)) } + quote! { Some(generator.subschema_for::<#pt>()) } } } else { quote! { None } @@ -127,7 +127,29 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { if is_json_value(ok_ty) { quote! { None } } else { - quote! { Some(schemars::schema_for!(#ok_ty)) } + quote! { Some(generator.subschema_for::<#ok_ty>()) } + } + } else { + quote! { None } + }; + + let params_name_expr = if let Some(pt) = &route.param_type { + if is_json_value(pt) { + quote! { None } + } else { + let name = type_name(pt); + quote! { Some(#name.to_string()) } + } + } else { + quote! { None } + }; + + let response_name_expr = if let Some(ok_ty) = &route.ok_type { + if is_json_value(ok_ty) { + quote! { None } + } else { + let name = type_name(ok_ty); + quote! { Some(#name.to_string()) } } } else { quote! { None } @@ -137,7 +159,9 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { crate::custom_requests::CustomMethodSchema { method: #full_method.to_string(), params_schema: #params_expr, + params_type_name: #params_name_expr, response_schema: #response_expr, + response_type_name: #response_name_expr, } } }) @@ -159,7 +183,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { // Generate the custom_method_schemas method. let schemas_fn = quote! { - pub fn custom_method_schemas() -> Vec { + pub fn custom_method_schemas(generator: &mut schemars::SchemaGenerator) -> Vec { vec![ #(#schema_entries),* ] @@ -232,6 +256,17 @@ fn extract_result_ok_type(sig: &syn::Signature) -> Option { None } +/// Extract the last segment name from a type path (e.g. `GetSessionRequest` from +/// `crate::custom_requests::GetSessionRequest` or just `GetSessionRequest`). +fn type_name(ty: &Type) -> String { + if let Type::Path(type_path) = ty { + if let Some(seg) = type_path.path.segments.last() { + return seg.ident.to_string(); + } + } + quote::quote!(#ty).to_string() +} + /// Check if a type is `serde_json::Value` (matches `Value` or `serde_json::Value`). fn is_json_value(ty: &Type) -> bool { if let Type::Path(type_path) = ty { diff --git a/crates/goose-acp/acp-meta.json b/crates/goose-acp/acp-meta.json new file mode 100644 index 000000000000..e8fe4ba92fd8 --- /dev/null +++ b/crates/goose-acp/acp-meta.json @@ -0,0 +1,94 @@ +{ + "methods": [ + { + "method": "extensions/add", + "requestType": "AddExtensionRequest", + "responseType": "EmptyResponse" + }, + { + "method": "extensions/remove", + "requestType": "RemoveExtensionRequest", + "responseType": "EmptyResponse" + }, + { + "method": "tools", + "requestType": "GetToolsRequest", + "responseType": "GetToolsResponse" + }, + { + "method": "resource/read", + "requestType": "ReadResourceRequest", + "responseType": "ReadResourceResponse" + }, + { + "method": "working_dir/update", + "requestType": "UpdateWorkingDirRequest", + "responseType": "EmptyResponse" + }, + { + "method": "session/list", + "requestType": null, + "responseType": "ListSessionsResponse" + }, + { + "method": "session/get", + "requestType": "GetSessionRequest", + "responseType": "GetSessionResponse" + }, + { + "method": "session/delete", + "requestType": "DeleteSessionRequest", + "responseType": "EmptyResponse" + }, + { + "method": "session/export", + "requestType": "ExportSessionRequest", + "responseType": "ExportSessionResponse" + }, + { + "method": "session/import", + "requestType": "ImportSessionRequest", + "responseType": "ImportSessionResponse" + }, + { + "method": "config/extensions", + "requestType": null, + "responseType": "GetExtensionsResponse" + }, + { + "method": "tool/call", + "requestType": null, + "responseType": null + }, + { + "method": "provider/update", + "requestType": null, + "responseType": null + }, + { + "method": "container/set", + "requestType": null, + "responseType": null + }, + { + "method": "apps/list", + "requestType": null, + "responseType": null + }, + { + "method": "apps/export", + "requestType": null, + "responseType": null + }, + { + "method": "apps/import", + "requestType": null, + "responseType": null + }, + { + "method": "config/providers", + "requestType": null, + "responseType": null + } + ] +} diff --git a/crates/goose-acp/acp-schema.json b/crates/goose-acp/acp-schema.json new file mode 100644 index 000000000000..f090ccf919cf --- /dev/null +++ b/crates/goose-acp/acp-schema.json @@ -0,0 +1,520 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "GooseExtensions", + "$defs": { + "AddExtensionRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "config": { + "description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform)." + } + }, + "required": [ + "session_id", + "config" + ], + "description": "Add an extension to an active session.\nMethod: `_agent/extensions/add`", + "x-side": "agent", + "x-method": "extensions/add" + }, + "EmptyResponse": { + "type": "object", + "description": "Empty success response for operations that return no data.", + "x-side": "agent" + }, + "RemoveExtensionRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "session_id", + "name" + ], + "description": "Remove an extension from an active session.\nMethod: `_agent/extensions/remove`", + "x-side": "agent", + "x-method": "extensions/remove" + }, + "GetToolsRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + } + }, + "required": [ + "session_id" + ], + "description": "List all tools available in a session.\nMethod: `_agent/tools`", + "x-side": "agent", + "x-method": "tools" + }, + "GetToolsResponse": { + "type": "object", + "properties": { + "tools": { + "type": "array", + "items": true, + "description": "Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`." + } + }, + "required": [ + "tools" + ], + "x-side": "agent", + "x-method": "tools" + }, + "ReadResourceRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "extension_name": { + "type": "string" + } + }, + "required": [ + "session_id", + "uri", + "extension_name" + ], + "description": "Read a resource from an extension.\nMethod: `_agent/resource/read`", + "x-side": "agent", + "x-method": "resource/read" + }, + "ReadResourceResponse": { + "type": "object", + "properties": { + "result": { + "description": "The resource result from the extension (MCP ReadResourceResult)." + } + }, + "required": [ + "result" + ], + "x-side": "agent", + "x-method": "resource/read" + }, + "UpdateWorkingDirRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "working_dir": { + "type": "string" + } + }, + "required": [ + "session_id", + "working_dir" + ], + "description": "Update the working directory for a session.\nMethod: `_agent/working_dir/update`", + "x-side": "agent", + "x-method": "working_dir/update" + }, + "ListSessionsResponse": { + "type": "object", + "properties": { + "sessions": { + "type": "array", + "items": true + } + }, + "required": [ + "sessions" + ], + "description": "List all sessions.\nMethod: `_session/list`", + "x-side": "agent", + "x-method": "session/list" + }, + "GetSessionRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "include_messages": { + "type": "boolean", + "default": false + } + }, + "required": [ + "session_id" + ], + "description": "Get a session by ID.\nMethod: `_session/get`", + "x-side": "agent", + "x-method": "session/get" + }, + "GetSessionResponse": { + "type": "object", + "properties": { + "session": { + "description": "The session object with id, name, working_dir, timestamps, tokens, etc." + } + }, + "required": [ + "session" + ], + "description": "Get a session response.", + "x-side": "agent", + "x-method": "session/get" + }, + "DeleteSessionRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + } + }, + "required": [ + "session_id" + ], + "description": "Delete a session.\nMethod: `_session/delete`", + "x-side": "agent", + "x-method": "session/delete" + }, + "ExportSessionRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + } + }, + "required": [ + "session_id" + ], + "description": "Export a session as a JSON string.\nMethod: `_session/export`", + "x-side": "agent", + "x-method": "session/export" + }, + "ExportSessionResponse": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + }, + "required": [ + "data" + ], + "x-side": "agent", + "x-method": "session/export" + }, + "ImportSessionRequest": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + }, + "required": [ + "data" + ], + "description": "Import a session from a JSON string.\nMethod: `_session/import`", + "x-side": "agent", + "x-method": "session/import" + }, + "ImportSessionResponse": { + "type": "object", + "properties": { + "session": { + "description": "The imported session object." + } + }, + "required": [ + "session" + ], + "x-side": "agent", + "x-method": "session/import" + }, + "GetExtensionsResponse": { + "type": "object", + "properties": { + "extensions": { + "type": "array", + "items": true, + "description": "Array of ExtensionEntry objects with `enabled` flag and config details." + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "extensions", + "warnings" + ], + "description": "List configured extensions and any warnings.\nMethod: `_config/extensions`", + "x-side": "agent", + "x-method": "config/extensions" + }, + "ExtRequest": { + "properties": { + "id": { + "type": "string" + }, + "method": { + "type": "string" + }, + "params": { + "anyOf": [ + { + "anyOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/AddExtensionRequest" + } + ], + "description": "Params for _goose/extensions/add", + "title": "AddExtensionRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/RemoveExtensionRequest" + } + ], + "description": "Params for _goose/extensions/remove", + "title": "RemoveExtensionRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetToolsRequest" + } + ], + "description": "Params for _goose/tools", + "title": "GetToolsRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ReadResourceRequest" + } + ], + "description": "Params for _goose/resource/read", + "title": "ReadResourceRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/UpdateWorkingDirRequest" + } + ], + "description": "Params for _goose/working_dir/update", + "title": "UpdateWorkingDirRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetSessionRequest" + } + ], + "description": "Params for _goose/session/get", + "title": "GetSessionRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/DeleteSessionRequest" + } + ], + "description": "Params for _goose/session/delete", + "title": "DeleteSessionRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ExportSessionRequest" + } + ], + "description": "Params for _goose/session/export", + "title": "ExportSessionRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ImportSessionRequest" + } + ], + "description": "Params for _goose/session/import", + "title": "ImportSessionRequest" + } + ] + }, + { + "description": "Untyped params", + "type": [ + "object", + "null" + ] + } + ] + } + }, + "required": [ + "id", + "method" + ], + "type": "object", + "x-docs-ignore": true + }, + "ExtResponse": { + "anyOf": [ + { + "properties": { + "id": { + "type": "string" + }, + "result": { + "anyOf": [ + { + "anyOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/EmptyResponse" + } + ], + "title": "EmptyResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetToolsResponse" + } + ], + "title": "GetToolsResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ReadResourceResponse" + } + ], + "title": "ReadResourceResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ListSessionsResponse" + } + ], + "title": "ListSessionsResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetSessionResponse" + } + ], + "title": "GetSessionResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ExportSessionResponse" + } + ], + "title": "ExportSessionResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ImportSessionResponse" + } + ], + "title": "ImportSessionResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetExtensionsResponse" + } + ], + "title": "GetExtensionsResponse" + } + ] + }, + { + "description": "Untyped result" + } + ] + } + }, + "required": [ + "id" + ], + "title": "Success", + "type": "object" + }, + { + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "code", + "message" + ] + }, + "id": { + "type": "string" + } + }, + "required": [ + "id", + "error" + ], + "title": "Error", + "type": "object" + } + ], + "x-docs-ignore": true + } + }, + "anyOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/ExtRequest" + } + ], + "description": "Extension request (client → agent)", + "title": "Request" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ExtResponse" + } + ], + "description": "Extension response (agent → client)", + "title": "Response" + } + ] +} diff --git a/crates/goose-acp/src/bin/generate_acp_schema.rs b/crates/goose-acp/src/bin/generate_acp_schema.rs index d3931a8b6fc6..688d4f4a7046 100644 --- a/crates/goose-acp/src/bin/generate_acp_schema.rs +++ b/crates/goose-acp/src/bin/generate_acp_schema.rs @@ -1,20 +1,188 @@ use goose_acp::server::GooseAcpAgent; +use schemars::SchemaGenerator; +use serde_json::{json, Map, Value}; +use std::collections::{BTreeSet, HashMap}; use std::env; use std::fs; use std::path::PathBuf; fn main() { - let schemas = GooseAcpAgent::custom_method_schemas(); - let json = serde_json::to_string_pretty(&schemas).expect("failed to serialize schemas"); + let mut generator = SchemaGenerator::default(); + let methods = GooseAcpAgent::custom_method_schemas(&mut generator); - let package_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let output_path = PathBuf::from(package_dir).join("acp-schema.json"); + // Collect $defs from the generator (all types referenced via subschema_for). + let mut defs: Map = generator + .take_definitions(true) + .into_iter() + .map(|(k, v)| (k, serde_json::to_value(v).unwrap_or(json!({})))) + .collect(); + + // Strip the `_goose/` prefix to get the bare method name for x-method. + fn bare_method(full: &str) -> &str { + full.strip_prefix("_goose/").unwrap_or(full) + } + + // Track which types map to which methods so we can detect shared types. + let mut type_methods: HashMap> = HashMap::new(); + for m in &methods { + let method = bare_method(&m.method).to_string(); + if let Some(name) = &m.params_type_name { + type_methods + .entry(name.clone()) + .or_default() + .push(method.clone()); + } + if let Some(name) = &m.response_type_name { + type_methods + .entry(name.clone()) + .or_default() + .push(method.clone()); + } + } + + // Annotate $defs entries with x-method/x-side. Only set x-method for types + // used by exactly one method (shared types like EmptyResponse skip x-method). + for (name, methods_list) in &type_methods { + if let Some(def) = defs.get_mut(name) { + if let Some(obj) = def.as_object_mut() { + obj.insert("x-side".into(), json!("agent")); + if methods_list.len() == 1 { + obj.insert("x-method".into(), json!(methods_list[0])); + } + } + } + } - fs::write(&output_path, format!("{json}\n")).expect("failed to write schema file"); - eprintln!( - "Generated ACP custom method schemas at {}", - output_path.display() + // Build ExtRequest.params and ExtResponse.result anyOf arrays, + // deduplicating response variants (e.g. EmptyResponse appears once). + let mut request_variants: Vec = Vec::new(); + let mut response_variants: Vec = Vec::new(); + let mut seen_response_types: BTreeSet = BTreeSet::new(); + + for m in &methods { + if let Some(name) = &m.params_type_name { + request_variants.push(json!({ + "allOf": [{ "$ref": format!("#/$defs/{name}") }], + "description": format!("Params for {}", m.method), + "title": name, + })); + } + + if let Some(name) = &m.response_type_name { + if seen_response_types.insert(name.clone()) { + response_variants.push(json!({ + "allOf": [{ "$ref": format!("#/$defs/{name}") }], + "title": name, + })); + } + } + } + + // Build ExtRequest — mirrors AgentRequest structure. + defs.insert( + "ExtRequest".into(), + json!({ + "properties": { + "id": { "type": "string" }, + "method": { "type": "string" }, + "params": { + "anyOf": [ + { "anyOf": request_variants }, + { "description": "Untyped params", "type": ["object", "null"] }, + ] + } + }, + "required": ["id", "method"], + "type": "object", + "x-docs-ignore": true, + }), ); - println!("{json}"); + // Build ExtResponse — mirrors AgentResponse structure. + defs.insert( + "ExtResponse".into(), + json!({ + "anyOf": [ + { + "properties": { + "id": { "type": "string" }, + "result": { + "anyOf": [ + { "anyOf": response_variants }, + { "description": "Untyped result" }, + ] + } + }, + "required": ["id"], + "title": "Success", + "type": "object", + }, + { + "properties": { + "error": { + "type": "object", + "properties": { + "code": { "type": "integer" }, + "message": { "type": "string" }, + "data": {} + }, + "required": ["code", "message"], + }, + "id": { "type": "string" }, + }, + "required": ["id", "error"], + "title": "Error", + "type": "object", + } + ], + "x-docs-ignore": true, + }), + ); + + // Assemble the root schema document. + let root = json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "GooseExtensions", + "$defs": defs, + "anyOf": [ + { + "allOf": [{ "$ref": "#/$defs/ExtRequest" }], + "description": "Extension request (client → agent)", + "title": "Request", + }, + { + "allOf": [{ "$ref": "#/$defs/ExtResponse" }], + "description": "Extension response (agent → client)", + "title": "Response", + } + ], + }); + + let json_str = serde_json::to_string_pretty(&root).expect("failed to serialize schema"); + + let package_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let package_path = PathBuf::from(&package_dir); + + let schema_path = package_path.join("acp-schema.json"); + fs::write(&schema_path, format!("{json_str}\n")).expect("failed to write schema file"); + eprintln!("Generated ACP schema at {}", schema_path.display()); + + // Build meta.json with method→type mappings (consumed by TS codegen). + let method_entries: Vec = methods + .iter() + .map(|m| { + json!({ + "method": bare_method(&m.method), + "requestType": m.params_type_name, + "responseType": m.response_type_name, + }) + }) + .collect(); + let meta = json!({ "methods": method_entries }); + let meta_str = serde_json::to_string_pretty(&meta).expect("failed to serialize meta"); + let meta_path = package_path.join("acp-meta.json"); + fs::write(&meta_path, format!("{meta_str}\n")).expect("failed to write meta file"); + eprintln!("Generated ACP meta at {}", meta_path.display()); + + println!("{json_str}"); } diff --git a/crates/goose-acp/src/custom_requests.rs b/crates/goose-acp/src/custom_requests.rs index 587776f9618b..41569a6827fd 100644 --- a/crates/goose-acp/src/custom_requests.rs +++ b/crates/goose-acp/src/custom_requests.rs @@ -7,11 +7,20 @@ use serde::{Deserialize, Serialize}; /// Schema descriptor for a single custom method, produced by the /// `#[custom_methods]` macro's generated `custom_method_schemas()` function. +/// +/// `params_schema` / `response_schema` hold `$ref` pointers or inline schemas +/// produced by `SchemaGenerator::subschema_for`. All referenced types are +/// collected in the generator's `$defs` map. +/// +/// `params_type_name` / `response_type_name` carry the Rust struct name so the +/// binary can key `$defs` entries and annotate them with `x-method` / `x-side`. #[derive(Debug, Serialize)] pub struct CustomMethodSchema { pub method: String, pub params_schema: Option, + pub params_type_name: Option, pub response_schema: Option, + pub response_type_name: Option, } // --------------------------------------------------------------------------- diff --git a/ui/desktop/generate-goose-ext-schema.ts b/ui/desktop/generate-goose-ext-schema.ts new file mode 100644 index 000000000000..04fea38d19b4 --- /dev/null +++ b/ui/desktop/generate-goose-ext-schema.ts @@ -0,0 +1,114 @@ +#!/usr/bin/env node +/** + * Generates TypeScript types + Zod validators for Goose custom extension methods. + * + * Usage: + * npx tsx generate-goose-ext-schema.ts + * npx tsx generate-goose-ext-schema.ts --skip-build # skip cargo build + */ + +import { createClient } from "@hey-api/openapi-ts"; +import { execSync } from "child_process"; +import * as fs from "fs/promises"; +import { resolve } from "path"; +import * as prettier from "prettier"; + +const SCHEMA_PATH = resolve( + __dirname, + "../../crates/goose-acp/acp-schema.json", +); +const META_PATH = resolve(__dirname, "../../crates/goose-acp/acp-meta.json"); +const OUTPUT_DIR = resolve(__dirname, "src/goose-ext-schema"); + +main().catch((err) => { + console.error(err); + process.exit(1); +}); + +async function main() { + // 1. Optionally rebuild the schema from Rust + if (!process.argv.includes("--skip-build")) { + console.log("Building Goose extension schema from Rust..."); + try { + execSync( + "source bin/activate-hermit && cargo run -p goose-acp --bin generate-acp-schema", + { + cwd: resolve(__dirname, "../.."), + stdio: "inherit", + shell: "/bin/zsh", + }, + ); + } catch { + console.error( + "Failed to build schema. Run with --skip-build to use existing files", + ); + process.exit(1); + } + } + + // 2. Read the JSON schema and metadata + const schemaSrc = await fs.readFile(SCHEMA_PATH, "utf8"); + const jsonSchema = JSON.parse( + // Convert JSON Schema $defs refs to OpenAPI component refs + schemaSrc.replaceAll("#/$defs/", "#/components/schemas/"), + ); + + const metaSrc = await fs.readFile(META_PATH, "utf8"); + const meta = JSON.parse(metaSrc); + + // 3. Generate TypeScript types + Zod validators via @hey-api/openapi-ts + await createClient({ + input: { + openapi: "3.1.0", + info: { + title: "Goose Extensions", + version: "1.0.0", + }, + components: { + schemas: jsonSchema.$defs, + }, + }, + output: { + path: OUTPUT_DIR, + }, + plugins: ["zod", "@hey-api/typescript"], + }); + + // 4. Post-process generated files + await postProcessTypes(); + await postProcessIndex(meta); + + console.log(`\nGenerated Goose extension schema in ${OUTPUT_DIR}`); +} + +async function postProcessTypes() { + const tsPath = resolve(OUTPUT_DIR, "types.gen.ts"); + let src = await fs.readFile(tsPath, "utf8"); + // Remove the ClientOptions type block injected by @hey-api (not part of our schema) + src = src.replace(/\nexport type ClientOptions =[\s\S]*?^};\n/m, "\n"); + await fs.writeFile(tsPath, src); +} + +async function postProcessIndex(meta: { methods: unknown[] }) { + const indexPath = resolve(OUTPUT_DIR, "index.ts"); + let src = await fs.readFile(indexPath, "utf8"); + + // Strip ClientOptions from re-exports + src = src.replace(/,?\s*ClientOptions\s*,?/g, (match) => { + if (match.startsWith(",") && match.endsWith(",")) return ","; + if (match.startsWith(",")) return ""; + return ""; + }); + + // Append method constants + const methodConstants = await prettier.format( + ` +export const GOOSE_EXT_METHODS = ${JSON.stringify(meta.methods, null, 2)} as const; + +export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number]; +`, + { parser: "typescript" }, + ); + + await fs.writeFile(indexPath, `${src}\n${methodConstants}`); +} diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 50127d4567ac..6fcf02b5c8de 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -219,7 +219,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -589,7 +588,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -630,7 +628,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1088,7 +1085,6 @@ "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", @@ -2722,7 +2718,6 @@ "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -3198,7 +3193,6 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "license": "MIT", - "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -6523,7 +6517,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6811,7 +6806,6 @@ "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -6853,7 +6847,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6864,7 +6857,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -7005,7 +6997,6 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -7391,7 +7382,6 @@ "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.18", "fflate": "^0.8.2", @@ -7640,7 +7630,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7713,7 +7702,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8254,7 +8242,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -9557,7 +9544,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -9615,7 +9603,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", @@ -10623,7 +10610,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11107,7 +11093,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -12337,7 +12322,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -13394,7 +13378,6 @@ "integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.7.6", @@ -14637,6 +14620,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -14657,7 +14641,6 @@ "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", @@ -17033,7 +17016,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -17135,6 +17117,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -17150,6 +17133,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -17505,7 +17489,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -17515,7 +17498,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -17537,7 +17519,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -19454,8 +19437,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -19986,7 +19968,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20411,7 +20392,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -20502,7 +20482,6 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -21151,7 +21130,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 78b2f52914bf..686cb7085056 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -39,7 +39,8 @@ "test:integration:watch": "vitest --config vitest.integration.config.ts", "test:integration:debug": "DEBUG=1 vitest run --config vitest.integration.config.ts", "prepare": "husky", - "start-alpha-gui": "ALPHA=true npm run start-gui" + "start-alpha-gui": "ALPHA=true npm run start-gui", + "generate-goose-ext-schema": "tsx generate-goose-ext-schema.ts" }, "dependencies": { "@mcp-ui/client": "^6.1.0", diff --git a/ui/desktop/src/goose-ext-schema/index.ts b/ui/desktop/src/goose-ext-schema/index.ts new file mode 100644 index 000000000000..f6cb96ea8930 --- /dev/null +++ b/ui/desktop/src/goose-ext-schema/index.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { + AddExtensionRequest, + DeleteSessionRequest, + EmptyResponse, + ExportSessionRequest, + ExportSessionResponse, + ExtRequest, + ExtResponse, + GetExtensionsResponse, + GetSessionRequest, + GetSessionResponse, + GetToolsRequest, + GetToolsResponse, + ImportSessionRequest, + ImportSessionResponse, + ListSessionsResponse, + ReadResourceRequest, + ReadResourceResponse, + RemoveExtensionRequest, + UpdateWorkingDirRequest, +} from './types.gen'; + +export const GOOSE_EXT_METHODS = [ + { + method: 'extensions/add', + requestType: 'AddExtensionRequest', + responseType: 'EmptyResponse', + }, + { + method: 'extensions/remove', + requestType: 'RemoveExtensionRequest', + responseType: 'EmptyResponse', + }, + { + method: 'tools', + requestType: 'GetToolsRequest', + responseType: 'GetToolsResponse', + }, + { + method: 'resource/read', + requestType: 'ReadResourceRequest', + responseType: 'ReadResourceResponse', + }, + { + method: 'working_dir/update', + requestType: 'UpdateWorkingDirRequest', + responseType: 'EmptyResponse', + }, + { + method: 'session/list', + requestType: null, + responseType: 'ListSessionsResponse', + }, + { + method: 'session/get', + requestType: 'GetSessionRequest', + responseType: 'GetSessionResponse', + }, + { + method: 'session/delete', + requestType: 'DeleteSessionRequest', + responseType: 'EmptyResponse', + }, + { + method: 'session/export', + requestType: 'ExportSessionRequest', + responseType: 'ExportSessionResponse', + }, + { + method: 'session/import', + requestType: 'ImportSessionRequest', + responseType: 'ImportSessionResponse', + }, + { + method: 'config/extensions', + requestType: null, + responseType: 'GetExtensionsResponse', + }, + { + method: 'tool/call', + requestType: null, + responseType: null, + }, + { + method: 'provider/update', + requestType: null, + responseType: null, + }, + { + method: 'container/set', + requestType: null, + responseType: null, + }, + { + method: 'apps/list', + requestType: null, + responseType: null, + }, + { + method: 'apps/export', + requestType: null, + responseType: null, + }, + { + method: 'apps/import', + requestType: null, + responseType: null, + }, + { + method: 'config/providers', + requestType: null, + responseType: null, + }, +] as const; + +export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number]; diff --git a/ui/desktop/src/goose-ext-schema/types.gen.ts b/ui/desktop/src/goose-ext-schema/types.gen.ts new file mode 100644 index 000000000000..ff20c25a9f41 --- /dev/null +++ b/ui/desktop/src/goose-ext-schema/types.gen.ts @@ -0,0 +1,186 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * Add an extension to an active session. + * Method: `_agent/extensions/add` + */ +export type AddExtensionRequest = { + session_id: string; + /** + * Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform). + */ + config: unknown; +}; + +/** + * Empty success response for operations that return no data. + */ +export type EmptyResponse = { + [key: string]: unknown; +}; + +/** + * Remove an extension from an active session. + * Method: `_agent/extensions/remove` + */ +export type RemoveExtensionRequest = { + session_id: string; + name: string; +}; + +/** + * List all tools available in a session. + * Method: `_agent/tools` + */ +export type GetToolsRequest = { + session_id: string; +}; + +export type GetToolsResponse = { + /** + * Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. + */ + tools: Array; +}; + +/** + * Read a resource from an extension. + * Method: `_agent/resource/read` + */ +export type ReadResourceRequest = { + session_id: string; + uri: string; + extension_name: string; +}; + +export type ReadResourceResponse = { + /** + * The resource result from the extension (MCP ReadResourceResult). + */ + result: unknown; +}; + +/** + * Update the working directory for a session. + * Method: `_agent/working_dir/update` + */ +export type UpdateWorkingDirRequest = { + session_id: string; + working_dir: string; +}; + +/** + * List all sessions. + * Method: `_session/list` + */ +export type ListSessionsResponse = { + sessions: Array; +}; + +/** + * Get a session by ID. + * Method: `_session/get` + */ +export type GetSessionRequest = { + session_id: string; + include_messages?: boolean; +}; + +/** + * Get a session response. + */ +export type GetSessionResponse = { + /** + * The session object with id, name, working_dir, timestamps, tokens, etc. + */ + session: unknown; +}; + +/** + * Delete a session. + * Method: `_session/delete` + */ +export type DeleteSessionRequest = { + session_id: string; +}; + +/** + * Export a session as a JSON string. + * Method: `_session/export` + */ +export type ExportSessionRequest = { + session_id: string; +}; + +export type ExportSessionResponse = { + data: string; +}; + +/** + * Import a session from a JSON string. + * Method: `_session/import` + */ +export type ImportSessionRequest = { + data: string; +}; + +export type ImportSessionResponse = { + /** + * The imported session object. + */ + session: unknown; +}; + +/** + * List configured extensions and any warnings. + * Method: `_config/extensions` + */ +export type GetExtensionsResponse = { + /** + * Array of ExtensionEntry objects with `enabled` flag and config details. + */ + extensions: Array; + warnings: Array; +}; + +export type ExtRequest = { + id: string; + method: string; + params?: + | AddExtensionRequest + | RemoveExtensionRequest + | GetToolsRequest + | ReadResourceRequest + | UpdateWorkingDirRequest + | GetSessionRequest + | DeleteSessionRequest + | ExportSessionRequest + | ImportSessionRequest + | { + [key: string]: unknown; + } + | null; +}; + +export type ExtResponse = + | { + id: string; + result?: + | EmptyResponse + | GetToolsResponse + | ReadResourceResponse + | ListSessionsResponse + | GetSessionResponse + | ExportSessionResponse + | ImportSessionResponse + | GetExtensionsResponse + | unknown; + } + | { + error: { + code: number; + message: string; + data?: unknown; + }; + id: string; + }; diff --git a/ui/desktop/src/goose-ext-schema/zod.gen.ts b/ui/desktop/src/goose-ext-schema/zod.gen.ts new file mode 100644 index 000000000000..9cfda4458ab9 --- /dev/null +++ b/ui/desktop/src/goose-ext-schema/zod.gen.ts @@ -0,0 +1,176 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +/** + * Add an extension to an active session. + * Method: `_agent/extensions/add` + */ +export const zAddExtensionRequest = z.object({ + session_id: z.string(), + config: z.unknown(), +}); + +/** + * Empty success response for operations that return no data. + */ +export const zEmptyResponse = z.record(z.unknown()); + +/** + * Remove an extension from an active session. + * Method: `_agent/extensions/remove` + */ +export const zRemoveExtensionRequest = z.object({ + session_id: z.string(), + name: z.string(), +}); + +/** + * List all tools available in a session. + * Method: `_agent/tools` + */ +export const zGetToolsRequest = z.object({ + session_id: z.string(), +}); + +export const zGetToolsResponse = z.object({ + tools: z.array(z.unknown()), +}); + +/** + * Read a resource from an extension. + * Method: `_agent/resource/read` + */ +export const zReadResourceRequest = z.object({ + session_id: z.string(), + uri: z.string(), + extension_name: z.string(), +}); + +export const zReadResourceResponse = z.object({ + result: z.unknown(), +}); + +/** + * Update the working directory for a session. + * Method: `_agent/working_dir/update` + */ +export const zUpdateWorkingDirRequest = z.object({ + session_id: z.string(), + working_dir: z.string(), +}); + +/** + * List all sessions. + * Method: `_session/list` + */ +export const zListSessionsResponse = z.object({ + sessions: z.array(z.unknown()), +}); + +/** + * Get a session by ID. + * Method: `_session/get` + */ +export const zGetSessionRequest = z.object({ + session_id: z.string(), + include_messages: z.boolean().optional().default(false), +}); + +/** + * Get a session response. + */ +export const zGetSessionResponse = z.object({ + session: z.unknown(), +}); + +/** + * Delete a session. + * Method: `_session/delete` + */ +export const zDeleteSessionRequest = z.object({ + session_id: z.string(), +}); + +/** + * Export a session as a JSON string. + * Method: `_session/export` + */ +export const zExportSessionRequest = z.object({ + session_id: z.string(), +}); + +export const zExportSessionResponse = z.object({ + data: z.string(), +}); + +/** + * Import a session from a JSON string. + * Method: `_session/import` + */ +export const zImportSessionRequest = z.object({ + data: z.string(), +}); + +export const zImportSessionResponse = z.object({ + session: z.unknown(), +}); + +/** + * List configured extensions and any warnings. + * Method: `_config/extensions` + */ +export const zGetExtensionsResponse = z.object({ + extensions: z.array(z.unknown()), + warnings: z.array(z.string()), +}); + +export const zExtRequest = z.object({ + id: z.string(), + method: z.string(), + params: z + .union([ + z.union([ + zAddExtensionRequest, + zRemoveExtensionRequest, + zGetToolsRequest, + zReadResourceRequest, + zUpdateWorkingDirRequest, + zGetSessionRequest, + zDeleteSessionRequest, + zExportSessionRequest, + zImportSessionRequest, + ]), + z.union([z.record(z.unknown()), z.null()]), + ]) + .optional(), +}); + +export const zExtResponse = z.union([ + z.object({ + id: z.string(), + result: z + .union([ + z.union([ + zEmptyResponse, + zGetToolsResponse, + zReadResourceResponse, + zListSessionsResponse, + zGetSessionResponse, + zExportSessionResponse, + zImportSessionResponse, + zGetExtensionsResponse, + ]), + z.unknown(), + ]) + .optional(), + }), + z.object({ + error: z.object({ + code: z.number().int(), + message: z.string(), + data: z.unknown().optional(), + }), + id: z.string(), + }), +]); From 845e0632f5b74546f5e0bf0bfbaac78bb1a63bd9 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 14:50:10 -0500 Subject: [PATCH 09/16] Move --- .../generate-schema.ts} | 23 +- ui/acp/package-lock.json | 1263 +++++++++++++++++ ui/acp/package.json | 23 + ui/acp/src/generated/index.ts | 98 ++ .../src/generated}/types.gen.ts | 133 +- .../src/generated}/zod.gen.ts | 131 +- ui/acp/src/index.ts | 1 + ui/acp/tsconfig.json | 14 + ui/desktop/package.json | 3 +- ui/desktop/src/goose-ext-schema/index.ts | 118 -- 10 files changed, 1533 insertions(+), 274 deletions(-) rename ui/{desktop/generate-goose-ext-schema.ts => acp/generate-schema.ts} (83%) create mode 100644 ui/acp/package-lock.json create mode 100644 ui/acp/package.json create mode 100644 ui/acp/src/generated/index.ts rename ui/{desktop/src/goose-ext-schema => acp/src/generated}/types.gen.ts (51%) rename ui/{desktop/src/goose-ext-schema => acp/src/generated}/zod.gen.ts (53%) create mode 100644 ui/acp/src/index.ts create mode 100644 ui/acp/tsconfig.json delete mode 100644 ui/desktop/src/goose-ext-schema/index.ts diff --git a/ui/desktop/generate-goose-ext-schema.ts b/ui/acp/generate-schema.ts similarity index 83% rename from ui/desktop/generate-goose-ext-schema.ts rename to ui/acp/generate-schema.ts index 04fea38d19b4..3a5fe238efdc 100644 --- a/ui/desktop/generate-goose-ext-schema.ts +++ b/ui/acp/generate-schema.ts @@ -3,22 +3,23 @@ * Generates TypeScript types + Zod validators for Goose custom extension methods. * * Usage: - * npx tsx generate-goose-ext-schema.ts - * npx tsx generate-goose-ext-schema.ts --skip-build # skip cargo build + * npm run generate # build Rust schema, then generate TS + * npm run generate:skip-build # use existing schema files */ import { createClient } from "@hey-api/openapi-ts"; import { execSync } from "child_process"; import * as fs from "fs/promises"; -import { resolve } from "path"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; import * as prettier from "prettier"; -const SCHEMA_PATH = resolve( - __dirname, - "../../crates/goose-acp/acp-schema.json", -); -const META_PATH = resolve(__dirname, "../../crates/goose-acp/acp-meta.json"); -const OUTPUT_DIR = resolve(__dirname, "src/goose-ext-schema"); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const ROOT = resolve(__dirname, "../.."); +const SCHEMA_PATH = resolve(ROOT, "crates/goose-acp/acp-schema.json"); +const META_PATH = resolve(ROOT, "crates/goose-acp/acp-meta.json"); +const OUTPUT_DIR = resolve(__dirname, "src/generated"); main().catch((err) => { console.error(err); @@ -33,14 +34,14 @@ async function main() { execSync( "source bin/activate-hermit && cargo run -p goose-acp --bin generate-acp-schema", { - cwd: resolve(__dirname, "../.."), + cwd: ROOT, stdio: "inherit", shell: "/bin/zsh", }, ); } catch { console.error( - "Failed to build schema. Run with --skip-build to use existing files", + "Failed to build schema. Run with --skip-build to use existing files.", ); process.exit(1); } diff --git a/ui/acp/package-lock.json b/ui/acp/package-lock.json new file mode 100644 index 000000000000..89e486d77c62 --- /dev/null +++ b/ui/acp/package-lock.json @@ -0,0 +1,1263 @@ +{ + "name": "@anthropic/goose-acp-types", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@anthropic/goose-acp-types", + "version": "0.1.0", + "dependencies": { + "zod": "^3.25.76" + }, + "devDependencies": { + "@hey-api/openapi-ts": "^0.92.3", + "prettier": "^3.8.1", + "tsx": "^4.21.0", + "typescript": "~5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hey-api/codegen-core": { + "version": "0.7.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@hey-api/codegen-core/-/codegen-core-0.7.0.tgz", + "integrity": "sha512-HglL4B4QwpzocE+c8qDU6XK8zMf8W8Pcv0RpFDYxHuYALWLTnpDUuEsglC7NQ4vC1maoXsBpMbmwpco0N4QviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.3", + "ansi-colors": "4.1.3", + "c12": "3.3.3", + "color-support": "1.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.3.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.3.0.tgz", + "integrity": "sha512-3tQJ8N2egHXZjQWUeceoWrl88APWjo7gRrQ/L4HWJKnh6HowczCv7yNNFeSusPoWGV6HGdoFiCvq6UsLkrwKhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "7.1.3", + "@types/json-schema": "7.0.15", + "js-yaml": "4.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.92.4", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@hey-api/openapi-ts/-/openapi-ts-0.92.4.tgz", + "integrity": "sha512-RA3wnL7Odr5xczuS3xpvnPClgJ/K8jivK3hvD8J0m5GBuvJFkZ1A1xp+6Ve1G0BV8p4LwxwgN1Qhb+4BFsLfMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.7.0", + "@hey-api/json-schema-ref-parser": "1.3.0", + "@hey-api/shared": "0.2.0", + "@hey-api/types": "0.1.3", + "ansi-colors": "4.1.3", + "color-support": "1.1.3", + "commander": "14.0.3" + }, + "bin": { + "openapi-ts": "bin/run.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/shared": { + "version": "0.2.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@hey-api/shared/-/shared-0.2.0.tgz", + "integrity": "sha512-t7C+65ES12OqAE5k6DB/y5nDuTjydtqdxf/Qe4zflVn2AzGs7hO/7KjXvGXZYnpNVF7QISAcj0LEObASU9I53Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.7.0", + "@hey-api/json-schema-ref-parser": "1.3.0", + "@hey-api/types": "0.1.3", + "ansi-colors": "4.1.3", + "cross-spawn": "7.0.6", + "open": "11.0.0", + "semver": "7.7.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/types": { + "version": "0.1.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@hey-api/types/-/types-0.1.3.tgz", + "integrity": "sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c12": { + "version": "3.3.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/c12/-/c12-3.3.3.tgz", + "integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^17.2.3", + "exsolve": "^1.0.8", + "giget": "^2.0.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/ui/acp/package.json b/ui/acp/package.json new file mode 100644 index 000000000000..0c9e792b06b0 --- /dev/null +++ b/ui/acp/package.json @@ -0,0 +1,23 @@ +{ + "name": "@anthropic/goose-acp-types", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "generate": "tsx generate-schema.ts", + "generate:skip-build": "tsx generate-schema.ts --skip-build", + "lint": "tsc --noEmit", + "format": "prettier --write src/" + }, + "dependencies": { + "zod": "^3.25.76" + }, + "devDependencies": { + "@hey-api/openapi-ts": "^0.92.3", + "prettier": "^3.8.1", + "tsx": "^4.21.0", + "typescript": "~5.9.3" + } +} diff --git a/ui/acp/src/generated/index.ts b/ui/acp/src/generated/index.ts new file mode 100644 index 000000000000..c1b3caff8017 --- /dev/null +++ b/ui/acp/src/generated/index.ts @@ -0,0 +1,98 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListSessionsResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen'; + +export const GOOSE_EXT_METHODS = [ + { + method: "extensions/add", + requestType: "AddExtensionRequest", + responseType: "EmptyResponse", + }, + { + method: "extensions/remove", + requestType: "RemoveExtensionRequest", + responseType: "EmptyResponse", + }, + { + method: "tools", + requestType: "GetToolsRequest", + responseType: "GetToolsResponse", + }, + { + method: "resource/read", + requestType: "ReadResourceRequest", + responseType: "ReadResourceResponse", + }, + { + method: "working_dir/update", + requestType: "UpdateWorkingDirRequest", + responseType: "EmptyResponse", + }, + { + method: "session/list", + requestType: null, + responseType: "ListSessionsResponse", + }, + { + method: "session/get", + requestType: "GetSessionRequest", + responseType: "GetSessionResponse", + }, + { + method: "session/delete", + requestType: "DeleteSessionRequest", + responseType: "EmptyResponse", + }, + { + method: "session/export", + requestType: "ExportSessionRequest", + responseType: "ExportSessionResponse", + }, + { + method: "session/import", + requestType: "ImportSessionRequest", + responseType: "ImportSessionResponse", + }, + { + method: "config/extensions", + requestType: null, + responseType: "GetExtensionsResponse", + }, + { + method: "tool/call", + requestType: null, + responseType: null, + }, + { + method: "provider/update", + requestType: null, + responseType: null, + }, + { + method: "container/set", + requestType: null, + responseType: null, + }, + { + method: "apps/list", + requestType: null, + responseType: null, + }, + { + method: "apps/export", + requestType: null, + responseType: null, + }, + { + method: "apps/import", + requestType: null, + responseType: null, + }, + { + method: "config/providers", + requestType: null, + responseType: null, + }, +] as const; + +export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number]; diff --git a/ui/desktop/src/goose-ext-schema/types.gen.ts b/ui/acp/src/generated/types.gen.ts similarity index 51% rename from ui/desktop/src/goose-ext-schema/types.gen.ts rename to ui/acp/src/generated/types.gen.ts index ff20c25a9f41..e404c8f82c98 100644 --- a/ui/desktop/src/goose-ext-schema/types.gen.ts +++ b/ui/acp/src/generated/types.gen.ts @@ -1,22 +1,23 @@ // This file is auto-generated by @hey-api/openapi-ts + /** * Add an extension to an active session. * Method: `_agent/extensions/add` */ export type AddExtensionRequest = { - session_id: string; - /** - * Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform). - */ - config: unknown; + session_id: string; + /** + * Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform). + */ + config: unknown; }; /** * Empty success response for operations that return no data. */ export type EmptyResponse = { - [key: string]: unknown; + [key: string]: unknown; }; /** @@ -24,8 +25,8 @@ export type EmptyResponse = { * Method: `_agent/extensions/remove` */ export type RemoveExtensionRequest = { - session_id: string; - name: string; + session_id: string; + name: string; }; /** @@ -33,14 +34,14 @@ export type RemoveExtensionRequest = { * Method: `_agent/tools` */ export type GetToolsRequest = { - session_id: string; + session_id: string; }; export type GetToolsResponse = { - /** - * Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. - */ - tools: Array; + /** + * Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. + */ + tools: Array; }; /** @@ -48,16 +49,16 @@ export type GetToolsResponse = { * Method: `_agent/resource/read` */ export type ReadResourceRequest = { - session_id: string; - uri: string; - extension_name: string; + session_id: string; + uri: string; + extension_name: string; }; export type ReadResourceResponse = { - /** - * The resource result from the extension (MCP ReadResourceResult). - */ - result: unknown; + /** + * The resource result from the extension (MCP ReadResourceResult). + */ + result: unknown; }; /** @@ -65,8 +66,8 @@ export type ReadResourceResponse = { * Method: `_agent/working_dir/update` */ export type UpdateWorkingDirRequest = { - session_id: string; - working_dir: string; + session_id: string; + working_dir: string; }; /** @@ -74,7 +75,7 @@ export type UpdateWorkingDirRequest = { * Method: `_session/list` */ export type ListSessionsResponse = { - sessions: Array; + sessions: Array; }; /** @@ -82,18 +83,18 @@ export type ListSessionsResponse = { * Method: `_session/get` */ export type GetSessionRequest = { - session_id: string; - include_messages?: boolean; + session_id: string; + include_messages?: boolean; }; /** * Get a session response. */ export type GetSessionResponse = { - /** - * The session object with id, name, working_dir, timestamps, tokens, etc. - */ - session: unknown; + /** + * The session object with id, name, working_dir, timestamps, tokens, etc. + */ + session: unknown; }; /** @@ -101,7 +102,7 @@ export type GetSessionResponse = { * Method: `_session/delete` */ export type DeleteSessionRequest = { - session_id: string; + session_id: string; }; /** @@ -109,11 +110,11 @@ export type DeleteSessionRequest = { * Method: `_session/export` */ export type ExportSessionRequest = { - session_id: string; + session_id: string; }; export type ExportSessionResponse = { - data: string; + data: string; }; /** @@ -121,14 +122,14 @@ export type ExportSessionResponse = { * Method: `_session/import` */ export type ImportSessionRequest = { - data: string; + data: string; }; export type ImportSessionResponse = { - /** - * The imported session object. - */ - session: unknown; + /** + * The imported session object. + */ + session: unknown; }; /** @@ -136,51 +137,29 @@ export type ImportSessionResponse = { * Method: `_config/extensions` */ export type GetExtensionsResponse = { - /** - * Array of ExtensionEntry objects with `enabled` flag and config details. - */ - extensions: Array; - warnings: Array; + /** + * Array of ExtensionEntry objects with `enabled` flag and config details. + */ + extensions: Array; + warnings: Array; }; export type ExtRequest = { - id: string; - method: string; - params?: - | AddExtensionRequest - | RemoveExtensionRequest - | GetToolsRequest - | ReadResourceRequest - | UpdateWorkingDirRequest - | GetSessionRequest - | DeleteSessionRequest - | ExportSessionRequest - | ImportSessionRequest - | { + id: string; + method: string; + params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | GetSessionRequest | DeleteSessionRequest | ExportSessionRequest | ImportSessionRequest | { [key: string]: unknown; - } - | null; -}; - -export type ExtResponse = - | { - id: string; - result?: - | EmptyResponse - | GetToolsResponse - | ReadResourceResponse - | ListSessionsResponse - | GetSessionResponse - | ExportSessionResponse - | ImportSessionResponse - | GetExtensionsResponse - | unknown; - } - | { - error: { + } | null; +}; + +export type ExtResponse = { + id: string; + result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | ListSessionsResponse | GetSessionResponse | ExportSessionResponse | ImportSessionResponse | GetExtensionsResponse | unknown; +} | { + error: { code: number; message: string; data?: unknown; - }; - id: string; }; + id: string; +}; diff --git a/ui/desktop/src/goose-ext-schema/zod.gen.ts b/ui/acp/src/generated/zod.gen.ts similarity index 53% rename from ui/desktop/src/goose-ext-schema/zod.gen.ts rename to ui/acp/src/generated/zod.gen.ts index 9cfda4458ab9..24cc5277007c 100644 --- a/ui/desktop/src/goose-ext-schema/zod.gen.ts +++ b/ui/acp/src/generated/zod.gen.ts @@ -7,8 +7,8 @@ import { z } from 'zod'; * Method: `_agent/extensions/add` */ export const zAddExtensionRequest = z.object({ - session_id: z.string(), - config: z.unknown(), + session_id: z.string(), + config: z.unknown() }); /** @@ -21,8 +21,8 @@ export const zEmptyResponse = z.record(z.unknown()); * Method: `_agent/extensions/remove` */ export const zRemoveExtensionRequest = z.object({ - session_id: z.string(), - name: z.string(), + session_id: z.string(), + name: z.string() }); /** @@ -30,11 +30,11 @@ export const zRemoveExtensionRequest = z.object({ * Method: `_agent/tools` */ export const zGetToolsRequest = z.object({ - session_id: z.string(), + session_id: z.string() }); export const zGetToolsResponse = z.object({ - tools: z.array(z.unknown()), + tools: z.array(z.unknown()) }); /** @@ -42,13 +42,13 @@ export const zGetToolsResponse = z.object({ * Method: `_agent/resource/read` */ export const zReadResourceRequest = z.object({ - session_id: z.string(), - uri: z.string(), - extension_name: z.string(), + session_id: z.string(), + uri: z.string(), + extension_name: z.string() }); export const zReadResourceResponse = z.object({ - result: z.unknown(), + result: z.unknown() }); /** @@ -56,8 +56,8 @@ export const zReadResourceResponse = z.object({ * Method: `_agent/working_dir/update` */ export const zUpdateWorkingDirRequest = z.object({ - session_id: z.string(), - working_dir: z.string(), + session_id: z.string(), + working_dir: z.string() }); /** @@ -65,7 +65,7 @@ export const zUpdateWorkingDirRequest = z.object({ * Method: `_session/list` */ export const zListSessionsResponse = z.object({ - sessions: z.array(z.unknown()), + sessions: z.array(z.unknown()) }); /** @@ -73,15 +73,15 @@ export const zListSessionsResponse = z.object({ * Method: `_session/get` */ export const zGetSessionRequest = z.object({ - session_id: z.string(), - include_messages: z.boolean().optional().default(false), + session_id: z.string(), + include_messages: z.boolean().optional().default(false) }); /** * Get a session response. */ export const zGetSessionResponse = z.object({ - session: z.unknown(), + session: z.unknown() }); /** @@ -89,7 +89,7 @@ export const zGetSessionResponse = z.object({ * Method: `_session/delete` */ export const zDeleteSessionRequest = z.object({ - session_id: z.string(), + session_id: z.string() }); /** @@ -97,11 +97,11 @@ export const zDeleteSessionRequest = z.object({ * Method: `_session/export` */ export const zExportSessionRequest = z.object({ - session_id: z.string(), + session_id: z.string() }); export const zExportSessionResponse = z.object({ - data: z.string(), + data: z.string() }); /** @@ -109,11 +109,11 @@ export const zExportSessionResponse = z.object({ * Method: `_session/import` */ export const zImportSessionRequest = z.object({ - data: z.string(), + data: z.string() }); export const zImportSessionResponse = z.object({ - session: z.unknown(), + session: z.unknown() }); /** @@ -121,56 +121,55 @@ export const zImportSessionResponse = z.object({ * Method: `_config/extensions` */ export const zGetExtensionsResponse = z.object({ - extensions: z.array(z.unknown()), - warnings: z.array(z.string()), + extensions: z.array(z.unknown()), + warnings: z.array(z.string()) }); export const zExtRequest = z.object({ - id: z.string(), - method: z.string(), - params: z - .union([ - z.union([ - zAddExtensionRequest, - zRemoveExtensionRequest, - zGetToolsRequest, - zReadResourceRequest, - zUpdateWorkingDirRequest, - zGetSessionRequest, - zDeleteSessionRequest, - zExportSessionRequest, - zImportSessionRequest, - ]), - z.union([z.record(z.unknown()), z.null()]), - ]) - .optional(), -}); - -export const zExtResponse = z.union([ - z.object({ id: z.string(), - result: z - .union([ + method: z.string(), + params: z.union([ z.union([ - zEmptyResponse, - zGetToolsResponse, - zReadResourceResponse, - zListSessionsResponse, - zGetSessionResponse, - zExportSessionResponse, - zImportSessionResponse, - zGetExtensionsResponse, + zAddExtensionRequest, + zRemoveExtensionRequest, + zGetToolsRequest, + zReadResourceRequest, + zUpdateWorkingDirRequest, + zGetSessionRequest, + zDeleteSessionRequest, + zExportSessionRequest, + zImportSessionRequest ]), - z.unknown(), - ]) - .optional(), - }), - z.object({ - error: z.object({ - code: z.number().int(), - message: z.string(), - data: z.unknown().optional(), + z.union([ + z.record(z.unknown()), + z.null() + ]) + ]).optional() +}); + +export const zExtResponse = z.union([ + z.object({ + id: z.string(), + result: z.union([ + z.union([ + zEmptyResponse, + zGetToolsResponse, + zReadResourceResponse, + zListSessionsResponse, + zGetSessionResponse, + zExportSessionResponse, + zImportSessionResponse, + zGetExtensionsResponse + ]), + z.unknown() + ]).optional() }), - id: z.string(), - }), + z.object({ + error: z.object({ + code: z.number().int(), + message: z.string(), + data: z.unknown().optional() + }), + id: z.string() + }) ]); diff --git a/ui/acp/src/index.ts b/ui/acp/src/index.ts new file mode 100644 index 000000000000..e84c86c6418a --- /dev/null +++ b/ui/acp/src/index.ts @@ -0,0 +1 @@ +export * from "./generated"; diff --git a/ui/acp/tsconfig.json b/ui/acp/tsconfig.json new file mode 100644 index 000000000000..8c18285d40de --- /dev/null +++ b/ui/acp/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 686cb7085056..78b2f52914bf 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -39,8 +39,7 @@ "test:integration:watch": "vitest --config vitest.integration.config.ts", "test:integration:debug": "DEBUG=1 vitest run --config vitest.integration.config.ts", "prepare": "husky", - "start-alpha-gui": "ALPHA=true npm run start-gui", - "generate-goose-ext-schema": "tsx generate-goose-ext-schema.ts" + "start-alpha-gui": "ALPHA=true npm run start-gui" }, "dependencies": { "@mcp-ui/client": "^6.1.0", diff --git a/ui/desktop/src/goose-ext-schema/index.ts b/ui/desktop/src/goose-ext-schema/index.ts deleted file mode 100644 index f6cb96ea8930..000000000000 --- a/ui/desktop/src/goose-ext-schema/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { - AddExtensionRequest, - DeleteSessionRequest, - EmptyResponse, - ExportSessionRequest, - ExportSessionResponse, - ExtRequest, - ExtResponse, - GetExtensionsResponse, - GetSessionRequest, - GetSessionResponse, - GetToolsRequest, - GetToolsResponse, - ImportSessionRequest, - ImportSessionResponse, - ListSessionsResponse, - ReadResourceRequest, - ReadResourceResponse, - RemoveExtensionRequest, - UpdateWorkingDirRequest, -} from './types.gen'; - -export const GOOSE_EXT_METHODS = [ - { - method: 'extensions/add', - requestType: 'AddExtensionRequest', - responseType: 'EmptyResponse', - }, - { - method: 'extensions/remove', - requestType: 'RemoveExtensionRequest', - responseType: 'EmptyResponse', - }, - { - method: 'tools', - requestType: 'GetToolsRequest', - responseType: 'GetToolsResponse', - }, - { - method: 'resource/read', - requestType: 'ReadResourceRequest', - responseType: 'ReadResourceResponse', - }, - { - method: 'working_dir/update', - requestType: 'UpdateWorkingDirRequest', - responseType: 'EmptyResponse', - }, - { - method: 'session/list', - requestType: null, - responseType: 'ListSessionsResponse', - }, - { - method: 'session/get', - requestType: 'GetSessionRequest', - responseType: 'GetSessionResponse', - }, - { - method: 'session/delete', - requestType: 'DeleteSessionRequest', - responseType: 'EmptyResponse', - }, - { - method: 'session/export', - requestType: 'ExportSessionRequest', - responseType: 'ExportSessionResponse', - }, - { - method: 'session/import', - requestType: 'ImportSessionRequest', - responseType: 'ImportSessionResponse', - }, - { - method: 'config/extensions', - requestType: null, - responseType: 'GetExtensionsResponse', - }, - { - method: 'tool/call', - requestType: null, - responseType: null, - }, - { - method: 'provider/update', - requestType: null, - responseType: null, - }, - { - method: 'container/set', - requestType: null, - responseType: null, - }, - { - method: 'apps/list', - requestType: null, - responseType: null, - }, - { - method: 'apps/export', - requestType: null, - responseType: null, - }, - { - method: 'apps/import', - requestType: null, - responseType: null, - }, - { - method: 'config/providers', - requestType: null, - responseType: null, - }, -] as const; - -export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number]; From e7742a132be236279e7c84ee9f42e7c552f1bfff Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 15:42:29 -0500 Subject: [PATCH 10/16] rename --- ui/acp/package-lock.json | 4 ++-- ui/acp/package.json | 2 +- ui/desktop/package-lock.json | 18 ++++++++++++++++++ ui/desktop/package.json | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ui/acp/package-lock.json b/ui/acp/package-lock.json index 89e486d77c62..6bcc51830972 100644 --- a/ui/acp/package-lock.json +++ b/ui/acp/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@anthropic/goose-acp-types", + "name": "goose-acp-types", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@anthropic/goose-acp-types", + "name": "goose-acp-types", "version": "0.1.0", "dependencies": { "zod": "^3.25.76" diff --git a/ui/acp/package.json b/ui/acp/package.json index 0c9e792b06b0..e1f1f2de9812 100644 --- a/ui/acp/package.json +++ b/ui/acp/package.json @@ -1,5 +1,5 @@ { - "name": "@anthropic/goose-acp-types", + "name": "goose-acp-types", "version": "0.1.0", "private": true, "type": "module", diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 6fcf02b5c8de..3b46f36a53eb 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -36,6 +36,7 @@ "electron-updater": "^6.7.3", "electron-window-state": "^5.0.3", "express": "^5.2.1", + "goose-acp-types": "file:../acp", "katex": "^0.16.28", "lodash": "^4.17.23", "lucide-react": "^0.563.0", @@ -120,6 +121,19 @@ "npm": "^11.6.1" } }, + "../acp": { + "name": "goose-acp-types", + "version": "0.1.0", + "dependencies": { + "zod": "^3.25.76" + }, + "devDependencies": { + "@hey-api/openapi-ts": "^0.92.3", + "prettier": "^3.8.1", + "tsx": "^4.21.0", + "typescript": "~5.9.3" + } + }, "node_modules/@acemir/cssom": { "version": "0.9.31", "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", @@ -11971,6 +11985,10 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goose-acp-types": { + "resolved": "../acp", + "link": true + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 78b2f52914bf..25e40d6d01a1 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -42,6 +42,7 @@ "start-alpha-gui": "ALPHA=true npm run start-gui" }, "dependencies": { + "goose-acp-types": "file:../acp", "@mcp-ui/client": "^6.1.0", "@modelcontextprotocol/ext-apps": "^1.0.1", "@radix-ui/react-accordion": "^1.2.12", From 94d07012e16f2249b7fb8de8198f3781faadefd8 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 15:44:32 -0500 Subject: [PATCH 11/16] Better client --- ui/acp/generate-schema.ts | 140 +++++++++++++++++++++++++++++ ui/acp/package-lock.json | 14 +++ ui/acp/package.json | 4 + ui/acp/src/generated/client.gen.ts | 129 ++++++++++++++++++++++++++ ui/acp/src/generated/index.ts | 2 +- ui/acp/src/index.ts | 4 +- 6 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 ui/acp/src/generated/client.gen.ts diff --git a/ui/acp/generate-schema.ts b/ui/acp/generate-schema.ts index 3a5fe238efdc..08cd402a4c10 100644 --- a/ui/acp/generate-schema.ts +++ b/ui/acp/generate-schema.ts @@ -79,6 +79,9 @@ async function main() { await postProcessTypes(); await postProcessIndex(meta); + // 5. Generate typed client wrapper + await generateClient(meta); + console.log(`\nGenerated Goose extension schema in ${OUTPUT_DIR}`); } @@ -101,6 +104,9 @@ async function postProcessIndex(meta: { methods: unknown[] }) { return ""; }); + // Fix bare relative imports to use .js extensions (required by nodenext consumers) + src = fixRelativeImports(src); + // Append method constants const methodConstants = await prettier.format( ` @@ -112,4 +118,138 @@ export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number]; ); await fs.writeFile(indexPath, `${src}\n${methodConstants}`); + + // Also fix imports in zod.gen.ts (it may import from types.gen) + for (const file of ["zod.gen.ts", "types.gen.ts"]) { + const filePath = resolve(OUTPUT_DIR, file); + try { + const content = await fs.readFile(filePath, "utf8"); + const fixed = fixRelativeImports(content); + if (fixed !== content) { + await fs.writeFile(filePath, fixed); + } + } catch { + // File may not exist + } + } +} + +function fixRelativeImports(src: string): string { + return src.replace( + /from\s+['"](\.[^'"]+)['"]/g, + (_match, importPath: string) => { + if (importPath.endsWith(".js") || importPath.endsWith(".json")) { + return `from '${importPath}'`; + } + return `from '${importPath}.js'`; + }, + ); +} + +interface MethodMeta { + method: string; + requestType: string | null; + responseType: string | null; +} + +/** + * Convert a method path like "session/list" or "working_dir/update" to camelCase "sessionList", "workingDirUpdate". + */ +function methodToCamelCase(method: string): string { + return method + .split(/[/_]/) + .map((part, i) => + i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1), + ) + .join(""); +} + +/** + * Generate a typed GooseClient class that wraps ClientSideConnection.extMethod() + * with proper TypeScript types and Zod runtime validation. + */ +async function generateClient(meta: { methods: MethodMeta[] }) { + const typeImports = new Set(); + const zodImports = new Set(); + + const methodDefs: string[] = []; + + for (const m of meta.methods) { + const fnName = methodToCamelCase(m.method); + const fullMethod = `_goose/${m.method}`; + + // Build param type and arg + let paramType = ""; + let paramArg = ""; + let callParams = "{}"; + if (m.requestType) { + typeImports.add(m.requestType); + paramType = m.requestType; + paramArg = `params: ${paramType}`; + callParams = "params"; + } + + // Build return type and validation + let returnType: string; + let bodyLines: string[]; + + if (m.responseType && m.responseType !== "EmptyResponse") { + typeImports.add(m.responseType); + const zodName = `z${m.responseType}`; + zodImports.add(zodName); + returnType = m.responseType; + bodyLines = [ + `const raw = await this.conn.extMethod("${fullMethod}", ${callParams});`, + `return ${zodName}.parse(raw) as ${returnType};`, + ]; + } else if (m.responseType === "EmptyResponse") { + returnType = "void"; + bodyLines = [ + `await this.conn.extMethod("${fullMethod}", ${callParams});`, + ]; + } else { + // Both request and response are untyped (serde_json::Value) + returnType = "Record"; + bodyLines = [ + `return await this.conn.extMethod("${fullMethod}", ${callParams ? callParams : "{}"});`, + ]; + } + + methodDefs.push(` + async ${fnName}(${paramArg}): Promise<${returnType}> { + ${bodyLines.join("\n ")} + }`); + } + + const typeImportLine = typeImports.size + ? `import type { ${[...typeImports].sort().join(", ")} } from "./types.gen.js";` + : ""; + const zodImportLine = zodImports.size + ? `import { ${[...zodImports].sort().join(", ")} } from "./zod.gen.js";` + : ""; + + let src = `// This file is auto-generated — do not edit manually. + +export interface ExtMethodProvider { + extMethod(method: string, params: Record): Promise>; +} + +${typeImportLine} +${zodImportLine} + +/** + * Typed client for Goose custom extension methods. + * Wraps an ExtMethodProvider (e.g. ClientSideConnection) with proper types and Zod validation. + */ +export class GooseClient { + constructor(private conn: ExtMethodProvider) {} +${methodDefs.join("\n")} +} +`; + + src = await prettier.format(src, { parser: "typescript" }); + src = fixRelativeImports(src); + + const clientPath = resolve(OUTPUT_DIR, "client.gen.ts"); + await fs.writeFile(clientPath, src); } diff --git a/ui/acp/package-lock.json b/ui/acp/package-lock.json index 6bcc51830972..3eb6dc181f85 100644 --- a/ui/acp/package-lock.json +++ b/ui/acp/package-lock.json @@ -11,10 +11,24 @@ "zod": "^3.25.76" }, "devDependencies": { + "@agentclientprotocol/sdk": "^0.14.1", "@hey-api/openapi-ts": "^0.92.3", "prettier": "^3.8.1", "tsx": "^4.21.0", "typescript": "~5.9.3" + }, + "peerDependencies": { + "@agentclientprotocol/sdk": "*" + } + }, + "node_modules/@agentclientprotocol/sdk": { + "version": "0.14.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/sdk/-/sdk-0.14.1.tgz", + "integrity": "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" } }, "node_modules/@esbuild/aix-ppc64": { diff --git a/ui/acp/package.json b/ui/acp/package.json index e1f1f2de9812..fc258fd648ec 100644 --- a/ui/acp/package.json +++ b/ui/acp/package.json @@ -14,7 +14,11 @@ "dependencies": { "zod": "^3.25.76" }, + "peerDependencies": { + "@agentclientprotocol/sdk": "*" + }, "devDependencies": { + "@agentclientprotocol/sdk": "^0.14.1", "@hey-api/openapi-ts": "^0.92.3", "prettier": "^3.8.1", "tsx": "^4.21.0", diff --git a/ui/acp/src/generated/client.gen.ts b/ui/acp/src/generated/client.gen.ts new file mode 100644 index 000000000000..db0ae5eb21d0 --- /dev/null +++ b/ui/acp/src/generated/client.gen.ts @@ -0,0 +1,129 @@ +// This file is auto-generated — do not edit manually. + +export interface ExtMethodProvider { + extMethod( + method: string, + params: Record, + ): Promise>; +} + +import type { + AddExtensionRequest, + DeleteSessionRequest, + ExportSessionRequest, + ExportSessionResponse, + GetExtensionsResponse, + GetSessionRequest, + GetSessionResponse, + GetToolsRequest, + GetToolsResponse, + ImportSessionRequest, + ImportSessionResponse, + ListSessionsResponse, + ReadResourceRequest, + ReadResourceResponse, + RemoveExtensionRequest, + UpdateWorkingDirRequest, +} from './types.gen.js'; +import { + zExportSessionResponse, + zGetExtensionsResponse, + zGetSessionResponse, + zGetToolsResponse, + zImportSessionResponse, + zListSessionsResponse, + zReadResourceResponse, +} from './zod.gen.js'; + +/** + * Typed client for Goose custom extension methods. + * Wraps an ExtMethodProvider (e.g. ClientSideConnection) with proper types and Zod validation. + */ +export class GooseClient { + constructor(private conn: ExtMethodProvider) {} + + async extensionsAdd(params: AddExtensionRequest): Promise { + await this.conn.extMethod("_goose/extensions/add", params); + } + + async extensionsRemove(params: RemoveExtensionRequest): Promise { + await this.conn.extMethod("_goose/extensions/remove", params); + } + + async tools(params: GetToolsRequest): Promise { + const raw = await this.conn.extMethod("_goose/tools", params); + return zGetToolsResponse.parse(raw) as GetToolsResponse; + } + + async resourceRead( + params: ReadResourceRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/resource/read", params); + return zReadResourceResponse.parse(raw) as ReadResourceResponse; + } + + async workingDirUpdate(params: UpdateWorkingDirRequest): Promise { + await this.conn.extMethod("_goose/working_dir/update", params); + } + + async sessionList(): Promise { + const raw = await this.conn.extMethod("_goose/session/list", {}); + return zListSessionsResponse.parse(raw) as ListSessionsResponse; + } + + async sessionGet(params: GetSessionRequest): Promise { + const raw = await this.conn.extMethod("_goose/session/get", params); + return zGetSessionResponse.parse(raw) as GetSessionResponse; + } + + async sessionDelete(params: DeleteSessionRequest): Promise { + await this.conn.extMethod("_goose/session/delete", params); + } + + async sessionExport( + params: ExportSessionRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/session/export", params); + return zExportSessionResponse.parse(raw) as ExportSessionResponse; + } + + async sessionImport( + params: ImportSessionRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/session/import", params); + return zImportSessionResponse.parse(raw) as ImportSessionResponse; + } + + async configExtensions(): Promise { + const raw = await this.conn.extMethod("_goose/config/extensions", {}); + return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse; + } + + async toolCall(): Promise> { + return await this.conn.extMethod("_goose/tool/call", {}); + } + + async providerUpdate(): Promise> { + return await this.conn.extMethod("_goose/provider/update", {}); + } + + async containerSet(): Promise> { + return await this.conn.extMethod("_goose/container/set", {}); + } + + async appsList(): Promise> { + return await this.conn.extMethod("_goose/apps/list", {}); + } + + async appsExport(): Promise> { + return await this.conn.extMethod("_goose/apps/export", {}); + } + + async appsImport(): Promise> { + return await this.conn.extMethod("_goose/apps/import", {}); + } + + async configProviders(): Promise> { + return await this.conn.extMethod("_goose/config/providers", {}); + } +} diff --git a/ui/acp/src/generated/index.ts b/ui/acp/src/generated/index.ts index c1b3caff8017..b44b56842762 100644 --- a/ui/acp/src/generated/index.ts +++ b/ui/acp/src/generated/index.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListSessionsResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen'; +export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListSessionsResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen.js'; export const GOOSE_EXT_METHODS = [ { diff --git a/ui/acp/src/index.ts b/ui/acp/src/index.ts index e84c86c6418a..04ae78458ede 100644 --- a/ui/acp/src/index.ts +++ b/ui/acp/src/index.ts @@ -1 +1,3 @@ -export * from "./generated"; +export * from "./generated/index.js"; +export * from "./generated/zod.gen.js"; +export { GooseClient } from "./generated/client.gen.js"; From c5d4472d63bf5c459d16ca7ee0fb92da81f0b037 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 16:16:25 -0500 Subject: [PATCH 12/16] Delete old test --- .../integration/acp-custom-requests.test.ts | 320 ------------------ 1 file changed, 320 deletions(-) delete mode 100644 ui/desktop/tests/integration/acp-custom-requests.test.ts diff --git a/ui/desktop/tests/integration/acp-custom-requests.test.ts b/ui/desktop/tests/integration/acp-custom-requests.test.ts deleted file mode 100644 index 2cc2242f6249..000000000000 --- a/ui/desktop/tests/integration/acp-custom-requests.test.ts +++ /dev/null @@ -1,320 +0,0 @@ -/* eslint-disable no-undef */ -/** - * Integration tests for goose-acp-server custom request methods. - * - * Spawns a real goose-acp-server process and sends JSON-RPC requests - * via HTTP+SSE to verify the custom _ handlers work end-to-end. - * - * Tests are split into two groups: - * 1. Session-independent: work with just an ACP session (initialize) - * 2. Session-dependent: require a goose session (session/new) which needs - * a configured provider - these are skipped in environments without one. - */ - -import { spawn, type ChildProcess } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; -import os from 'node:os'; -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; - -const ACP_SERVER_BINARY = path.resolve(__dirname, '../../../../target/debug/goose-acp-server'); - -interface JsonRpcResponse { - jsonrpc: string; - id?: number; - result?: unknown; - error?: { code: number; message: string; data?: unknown }; -} - -interface AcpTestContext { - baseUrl: string; - serverProcess: ChildProcess; - tempDir: string; - acpSessionId: string; - gooseSessionId: string | null; -} - -let ctx: AcpTestContext; - -/** - * Read an SSE stream from a fetch Response, collecting JSON-RPC messages. - * Resolves once a message with the expected `id` is found (or times out). - */ -async function readSseResponse( - response: Response, - expectedId: number, - timeoutMs = 10000 -): Promise<{ messages: JsonRpcResponse[]; headers: Headers }> { - const messages: JsonRpcResponse[] = []; - const reader = response.body!.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - const timeout = new Promise((_, reject) => - setTimeout(() => reject(new Error(`SSE timeout waiting for id=${expectedId}`)), timeoutMs) - ); - - const read = async (): Promise<{ messages: JsonRpcResponse[]; headers: Headers }> => { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - const trimmed = line.trim(); - if (trimmed.startsWith('data:')) { - const data = trimmed.slice('data:'.length).trim(); - if (data) { - try { - const parsed = JSON.parse(data) as JsonRpcResponse; - messages.push(parsed); - if (parsed.id === expectedId) { - reader.cancel().catch(() => {}); - return { messages, headers: response.headers }; - } - } catch { - // skip non-JSON data lines - } - } - } - } - } - return { messages, headers: response.headers }; - }; - - return Promise.race([read(), timeout]); -} - -/** - * Send a JSON-RPC request and wait for the matching response via SSE. - */ -async function sendJsonRpc( - baseUrl: string, - method: string, - params: Record, - id: number, - acpSessionId: string -): Promise { - const headers: Record = { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - 'Acp-Session-Id': acpSessionId, - }; - - const response = await fetch(`${baseUrl}/acp`, { - method: 'POST', - headers, - body: JSON.stringify({ jsonrpc: '2.0', method, params, id }), - }); - - const { messages } = await readSseResponse(response, id); - const match = messages.find((m) => m.id === id); - if (!match) { - throw new Error(`No response for id=${id}, method=${method}. Got: ${JSON.stringify(messages)}`); - } - return match; -} - -async function waitForServer(baseUrl: string, timeoutMs = 15000): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - try { - const resp = await fetch(`${baseUrl}/health`); - if (resp.ok) return; - } catch { - // not ready yet - } - await new Promise((r) => setTimeout(r, 200)); - } - throw new Error(`ACP server did not start within ${timeoutMs}ms`); -} - -async function initializeSession(baseUrl: string): Promise { - const response = await fetch(`${baseUrl}/acp`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - method: 'initialize', - params: { - protocolVersion: '2025-03-26', - clientInfo: { name: 'integration-test', version: '0.1.0' }, - capabilities: {}, - }, - id: 1, - }), - }); - - const acpSessionId = response.headers.get('acp-session-id'); - if (!acpSessionId) { - throw new Error(`No Acp-Session-Id header in initialize response`); - } - - // Consume the SSE stream - await readSseResponse(response, 1); - return acpSessionId; -} - -beforeAll(async () => { - if (!fs.existsSync(ACP_SERVER_BINARY)) { - throw new Error( - `Binary not found at ${ACP_SERVER_BINARY}. Run 'cargo build -p goose-acp' first.` - ); - } - - const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'goose-acp-test-')); - const port = 30000 + Math.floor(Math.random() * 10000); - const baseUrl = `http://127.0.0.1:${port}`; - - const serverProcess = spawn(ACP_SERVER_BINARY, ['--host', '127.0.0.1', '--port', String(port)], { - env: { ...process.env, GOOSE_PATH_ROOT: tempDir }, - stdio: ['ignore', 'pipe', 'pipe'], - }); - - serverProcess.stderr?.on('data', (data: Buffer) => { - if (process.env.DEBUG) console.error('[acp]', data.toString().trim()); - }); - - ctx = { - baseUrl, - serverProcess, - tempDir, - acpSessionId: '', - gooseSessionId: null, - }; - - console.log(`[ACP TEST] Starting server on port ${port}...`); - await waitForServer(baseUrl); - console.log(`[ACP TEST] Server ready. Initializing ACP session...`); - ctx.acpSessionId = await initializeSession(baseUrl); - console.log(`[ACP TEST] ACP session: ${ctx.acpSessionId}`); -}, 30000); - -afterAll(async () => { - if (ctx?.serverProcess) { - ctx.serverProcess.kill('SIGTERM'); - await new Promise((resolve) => { - ctx.serverProcess.on('close', () => resolve()); - setTimeout(() => { - ctx.serverProcess.kill('SIGKILL'); - resolve(); - }, 5000); - }); - } - if (ctx?.tempDir) { - await fs.promises.rm(ctx.tempDir, { recursive: true, force: true }).catch(() => {}); - } -}); - -describe('ACP custom requests - session independent', () => { - it('session/list returns a sessions array', async () => { - const response = await sendJsonRpc( - ctx.baseUrl, - '_goose/session/list', - {}, - 10, - ctx.acpSessionId - ); - - expect(response.error).toBeUndefined(); - expect(response.result).toBeDefined(); - - const result = response.result as { sessions: unknown[] }; - expect(Array.isArray(result.sessions)).toBe(true); - }); - - it('config/extensions returns extensions and warnings', async () => { - const response = await sendJsonRpc( - ctx.baseUrl, - '_goose/config/extensions', - {}, - 11, - ctx.acpSessionId - ); - - expect(response.error).toBeUndefined(); - const result = response.result as { extensions: unknown[]; warnings: unknown[] }; - expect(Array.isArray(result.extensions)).toBe(true); - expect(Array.isArray(result.warnings)).toBe(true); - }); - - it('unknown _ method returns method_not_found error', async () => { - const response = await sendJsonRpc(ctx.baseUrl, '_unknown/method', {}, 12, ctx.acpSessionId); - - expect(response.error).toBeDefined(); - expect(response.error!.code).toBe(-32601); - }); -}); - -describe('ACP custom requests - session dependent', () => { - it('_session/get retrieves a session', async () => { - if (!ctx.gooseSessionId) { - console.log('Skipping: no goose session (provider not configured)'); - return; - } - - const response = await sendJsonRpc( - ctx.baseUrl, - '_session/get', - { session_id: ctx.gooseSessionId }, - 20, - ctx.acpSessionId - ); - - expect(response.error).toBeUndefined(); - const result = response.result as { session: { id: string } }; - expect(result.session.id).toBe(ctx.gooseSessionId); - }); - - it('_agent/tools returns tools for a session', async () => { - if (!ctx.gooseSessionId) { - console.log('Skipping: no goose session (provider not configured)'); - return; - } - - const response = await sendJsonRpc( - ctx.baseUrl, - '_agent/tools', - { session_id: ctx.gooseSessionId }, - 21, - ctx.acpSessionId - ); - - expect(response.error).toBeUndefined(); - const result = response.result as { tools: unknown[] }; - expect(Array.isArray(result.tools)).toBe(true); - }); - - it('_session/delete removes a session', async () => { - if (!ctx.gooseSessionId) { - console.log('Skipping: no goose session (provider not configured)'); - return; - } - - const deleteResp = await sendJsonRpc( - ctx.baseUrl, - '_session/delete', - { session_id: ctx.gooseSessionId }, - 22, - ctx.acpSessionId - ); - expect(deleteResp.error).toBeUndefined(); - - // Verify it's gone - const getResp = await sendJsonRpc( - ctx.baseUrl, - '_session/get', - { session_id: ctx.gooseSessionId }, - 23, - ctx.acpSessionId - ); - expect(getResp.error).toBeDefined(); - }); -}); From 3aedb0ebc9b908170831018275c60cc735cfcfd2 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 16:26:37 -0500 Subject: [PATCH 13/16] don't build --- ui/acp/generate-schema.ts | 25 ------------------------- ui/acp/package.json | 1 - 2 files changed, 26 deletions(-) diff --git a/ui/acp/generate-schema.ts b/ui/acp/generate-schema.ts index 08cd402a4c10..ee63176c2daa 100644 --- a/ui/acp/generate-schema.ts +++ b/ui/acp/generate-schema.ts @@ -4,7 +4,6 @@ * * Usage: * npm run generate # build Rust schema, then generate TS - * npm run generate:skip-build # use existing schema files */ import { createClient } from "@hey-api/openapi-ts"; @@ -27,27 +26,6 @@ main().catch((err) => { }); async function main() { - // 1. Optionally rebuild the schema from Rust - if (!process.argv.includes("--skip-build")) { - console.log("Building Goose extension schema from Rust..."); - try { - execSync( - "source bin/activate-hermit && cargo run -p goose-acp --bin generate-acp-schema", - { - cwd: ROOT, - stdio: "inherit", - shell: "/bin/zsh", - }, - ); - } catch { - console.error( - "Failed to build schema. Run with --skip-build to use existing files.", - ); - process.exit(1); - } - } - - // 2. Read the JSON schema and metadata const schemaSrc = await fs.readFile(SCHEMA_PATH, "utf8"); const jsonSchema = JSON.parse( // Convert JSON Schema $defs refs to OpenAPI component refs @@ -57,7 +35,6 @@ async function main() { const metaSrc = await fs.readFile(META_PATH, "utf8"); const meta = JSON.parse(metaSrc); - // 3. Generate TypeScript types + Zod validators via @hey-api/openapi-ts await createClient({ input: { openapi: "3.1.0", @@ -75,11 +52,9 @@ async function main() { plugins: ["zod", "@hey-api/typescript"], }); - // 4. Post-process generated files await postProcessTypes(); await postProcessIndex(meta); - // 5. Generate typed client wrapper await generateClient(meta); console.log(`\nGenerated Goose extension schema in ${OUTPUT_DIR}`); diff --git a/ui/acp/package.json b/ui/acp/package.json index fc258fd648ec..94b98b968ede 100644 --- a/ui/acp/package.json +++ b/ui/acp/package.json @@ -7,7 +7,6 @@ "types": "./src/index.ts", "scripts": { "generate": "tsx generate-schema.ts", - "generate:skip-build": "tsx generate-schema.ts --skip-build", "lint": "tsc --noEmit", "format": "prettier --write src/" }, From 11ab36aacdf85ca50b4d07511001ad5587cf0c20 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 20:14:39 -0500 Subject: [PATCH 14/16] Clean up unimplemented --- crates/goose-acp/acp-meta.json | 35 --------------- crates/goose-acp/src/custom_requests.rs | 32 -------------- crates/goose-acp/src/server.rs | 59 ------------------------- 3 files changed, 126 deletions(-) diff --git a/crates/goose-acp/acp-meta.json b/crates/goose-acp/acp-meta.json index e8fe4ba92fd8..cbd9dd62f111 100644 --- a/crates/goose-acp/acp-meta.json +++ b/crates/goose-acp/acp-meta.json @@ -54,41 +54,6 @@ "method": "config/extensions", "requestType": null, "responseType": "GetExtensionsResponse" - }, - { - "method": "tool/call", - "requestType": null, - "responseType": null - }, - { - "method": "provider/update", - "requestType": null, - "responseType": null - }, - { - "method": "container/set", - "requestType": null, - "responseType": null - }, - { - "method": "apps/list", - "requestType": null, - "responseType": null - }, - { - "method": "apps/export", - "requestType": null, - "responseType": null - }, - { - "method": "apps/import", - "requestType": null, - "responseType": null - }, - { - "method": "config/providers", - "requestType": null, - "responseType": null } ] } diff --git a/crates/goose-acp/src/custom_requests.rs b/crates/goose-acp/src/custom_requests.rs index 41569a6827fd..8111b138c6f6 100644 --- a/crates/goose-acp/src/custom_requests.rs +++ b/crates/goose-acp/src/custom_requests.rs @@ -1,10 +1,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -// --------------------------------------------------------------------------- -// Schema metadata for custom methods -// --------------------------------------------------------------------------- - /// Schema descriptor for a single custom method, produced by the /// `#[custom_methods]` macro's generated `custom_method_schemas()` function. /// @@ -23,10 +19,6 @@ pub struct CustomMethodSchema { pub response_type_name: Option, } -// --------------------------------------------------------------------------- -// Agent: extensions -// --------------------------------------------------------------------------- - /// Add an extension to an active session. /// Method: `_agent/extensions/add` #[derive(Debug, Deserialize, JsonSchema)] @@ -44,10 +36,6 @@ pub struct RemoveExtensionRequest { pub name: String, } -// --------------------------------------------------------------------------- -// Agent: tools -// --------------------------------------------------------------------------- - /// List all tools available in a session. /// Method: `_agent/tools` #[derive(Debug, Deserialize, JsonSchema)] @@ -61,10 +49,6 @@ pub struct GetToolsResponse { pub tools: Vec, } -// --------------------------------------------------------------------------- -// Agent: resource -// --------------------------------------------------------------------------- - /// Read a resource from an extension. /// Method: `_agent/resource/read` #[derive(Debug, Deserialize, JsonSchema)] @@ -80,10 +64,6 @@ pub struct ReadResourceResponse { pub result: serde_json::Value, } -// --------------------------------------------------------------------------- -// Agent: working directory -// --------------------------------------------------------------------------- - /// Update the working directory for a session. /// Method: `_agent/working_dir/update` #[derive(Debug, Deserialize, JsonSchema)] @@ -92,10 +72,6 @@ pub struct UpdateWorkingDirRequest { pub working_dir: String, } -// --------------------------------------------------------------------------- -// Session management -// --------------------------------------------------------------------------- - /// Get a session by ID. /// Method: `_session/get` #[derive(Debug, Deserialize, JsonSchema)] @@ -151,10 +127,6 @@ pub struct ImportSessionResponse { pub session: serde_json::Value, } -// --------------------------------------------------------------------------- -// Config -// --------------------------------------------------------------------------- - /// List configured extensions and any warnings. /// Method: `_config/extensions` #[derive(Debug, Serialize, JsonSchema)] @@ -164,10 +136,6 @@ pub struct GetExtensionsResponse { pub warnings: Vec, } -// --------------------------------------------------------------------------- -// Shared empty response -// --------------------------------------------------------------------------- - /// Empty success response for operations that return no data. #[derive(Debug, Serialize, JsonSchema)] pub struct EmptyResponse {} diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 3ebeddd7a3a0..ecdc9742401d 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -1160,65 +1160,6 @@ impl GooseAcpAgent { }) } - #[custom_method("tool/call")] - async fn on_tool_call( - &self, - _req: serde_json::Value, - ) -> Result { - Err(sacp::Error::new(-32001, "tool/call not yet implemented")) - } - - #[custom_method("provider/update")] - async fn on_provider_update( - &self, - _req: serde_json::Value, - ) -> Result { - Err(sacp::Error::new( - -32001, - "provider/update not yet implemented", - )) - } - - #[custom_method("container/set")] - async fn on_container_set( - &self, - _req: serde_json::Value, - ) -> Result { - Err(sacp::Error::new( - -32001, - "container/set not yet implemented", - )) - } - - #[custom_method("apps/list")] - async fn on_apps_list(&self) -> Result { - Err(sacp::Error::new(-32001, "apps/list not yet implemented")) - } - - #[custom_method("apps/export")] - async fn on_apps_export( - &self, - _req: serde_json::Value, - ) -> Result { - Err(sacp::Error::new(-32001, "apps/export not yet implemented")) - } - - #[custom_method("apps/import")] - async fn on_apps_import( - &self, - _req: serde_json::Value, - ) -> Result { - Err(sacp::Error::new(-32001, "apps/import not yet implemented")) - } - - #[custom_method("config/providers")] - async fn on_config_providers(&self) -> Result { - Err(sacp::Error::new( - -32001, - "config/providers not yet implemented", - )) - } - async fn get_agent_for_session(&self, session_id: &str) -> Result, sacp::Error> { self.sessions .lock() From 2a2d92c8f68149f712d57fd42912a6c8ed9a6464 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 20:17:03 -0500 Subject: [PATCH 15/16] clean up method comments --- crates/goose-acp/src/custom_requests.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/crates/goose-acp/src/custom_requests.rs b/crates/goose-acp/src/custom_requests.rs index 8111b138c6f6..04025a4353fd 100644 --- a/crates/goose-acp/src/custom_requests.rs +++ b/crates/goose-acp/src/custom_requests.rs @@ -20,7 +20,6 @@ pub struct CustomMethodSchema { } /// Add an extension to an active session. -/// Method: `_agent/extensions/add` #[derive(Debug, Deserialize, JsonSchema)] pub struct AddExtensionRequest { pub session_id: String, @@ -29,7 +28,6 @@ pub struct AddExtensionRequest { } /// Remove an extension from an active session. -/// Method: `_agent/extensions/remove` #[derive(Debug, Deserialize, JsonSchema)] pub struct RemoveExtensionRequest { pub session_id: String, @@ -37,7 +35,6 @@ pub struct RemoveExtensionRequest { } /// List all tools available in a session. -/// Method: `_agent/tools` #[derive(Debug, Deserialize, JsonSchema)] pub struct GetToolsRequest { pub session_id: String, @@ -50,7 +47,6 @@ pub struct GetToolsResponse { } /// Read a resource from an extension. -/// Method: `_agent/resource/read` #[derive(Debug, Deserialize, JsonSchema)] pub struct ReadResourceRequest { pub session_id: String, @@ -65,7 +61,6 @@ pub struct ReadResourceResponse { } /// Update the working directory for a session. -/// Method: `_agent/working_dir/update` #[derive(Debug, Deserialize, JsonSchema)] pub struct UpdateWorkingDirRequest { pub session_id: String, @@ -73,7 +68,6 @@ pub struct UpdateWorkingDirRequest { } /// Get a session by ID. -/// Method: `_session/get` #[derive(Debug, Deserialize, JsonSchema)] pub struct GetSessionRequest { pub session_id: String, @@ -89,21 +83,18 @@ pub struct GetSessionResponse { } /// List all sessions. -/// Method: `_session/list` #[derive(Debug, Serialize, JsonSchema)] pub struct ListSessionsResponse { pub sessions: Vec, } /// Delete a session. -/// Method: `_session/delete` #[derive(Debug, Deserialize, JsonSchema)] pub struct DeleteSessionRequest { pub session_id: String, } /// Export a session as a JSON string. -/// Method: `_session/export` #[derive(Debug, Deserialize, JsonSchema)] pub struct ExportSessionRequest { pub session_id: String, @@ -115,7 +106,6 @@ pub struct ExportSessionResponse { } /// Import a session from a JSON string. -/// Method: `_session/import` #[derive(Debug, Deserialize, JsonSchema)] pub struct ImportSessionRequest { pub data: String, @@ -128,7 +118,6 @@ pub struct ImportSessionResponse { } /// List configured extensions and any warnings. -/// Method: `_config/extensions` #[derive(Debug, Serialize, JsonSchema)] pub struct GetExtensionsResponse { /// Array of ExtensionEntry objects with `enabled` flag and config details. From 73e015adc7da99cbbddd2019d9b10cb9294755fd Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 18 Feb 2026 21:03:04 -0500 Subject: [PATCH 16/16] Remove the test --- crates/goose-acp/tests/custom_requests_test.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/goose-acp/tests/custom_requests_test.rs b/crates/goose-acp/tests/custom_requests_test.rs index 862eaec99572..0f7c7678a929 100644 --- a/crates/goose-acp/tests/custom_requests_test.rs +++ b/crates/goose-acp/tests/custom_requests_test.rs @@ -168,20 +168,3 @@ fn test_custom_unknown_method() { assert!(result.is_err(), "expected method_not_found error"); }); } - -#[test] -fn test_custom_stubbed_method() { - run_test(async { - let openai = OpenAiFixture::new(vec![], ExpectedSessionId::default()).await; - let conn = ClientToAgentConnection::new(TestConnectionConfig::default(), openai).await; - - let result = send_custom(conn.cx(), "_goose/tool/call", serde_json::json!({})).await; - assert!(result.is_err(), "expected not-yet-implemented error"); - let err = result.unwrap_err(); - assert_eq!( - err.code, - sacp::ErrorCode::Other(-32001), - "expected custom error code -32001" - ); - }); -}