mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Typescript SDK for ACP extension methods (#7319)
This commit is contained in:
Generated
+11
@@ -4310,12 +4310,14 @@ dependencies = [
|
||||
"fs-err",
|
||||
"futures",
|
||||
"goose",
|
||||
"goose-acp-macros",
|
||||
"goose-mcp",
|
||||
"goose-test-support",
|
||||
"http-body-util",
|
||||
"regex",
|
||||
"rmcp 0.16.0",
|
||||
"sacp",
|
||||
"schemars 1.2.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
@@ -4330,6 +4332,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"
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,284 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
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 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<CustomMethodSchema>` 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
|
||||
///
|
||||
/// 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<ListSessionsResponse, sacp::Error> { .. }
|
||||
///
|
||||
/// // Typed params — JSON params auto-deserialized
|
||||
/// #[custom_method("session/get")]
|
||||
/// async fn on_get_session(&self, req: GetSessionRequest) -> Result<GetSessionResponse, sacp::Error> { .. }
|
||||
/// ```
|
||||
///
|
||||
/// The return type must be `Result<T, sacp::Error>` 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<Route> = 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::<Lit>() {
|
||||
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();
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(_) => {
|
||||
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 => {
|
||||
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 schema entries for each route using SchemaGenerator for $ref dedup.
|
||||
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(generator.subschema_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(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 }
|
||||
};
|
||||
|
||||
quote! {
|
||||
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,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Generate the handle_custom_request method.
|
||||
let dispatcher = quote! {
|
||||
async fn handle_custom_request(
|
||||
&self,
|
||||
method: &str,
|
||||
params: serde_json::Value,
|
||||
) -> Result<serde_json::Value, sacp::Error> {
|
||||
match method {
|
||||
#(#arms)*
|
||||
_ => Err(sacp::Error::method_not_found()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generate the custom_method_schemas method.
|
||||
let schemas_fn = quote! {
|
||||
pub fn custom_method_schemas(generator: &mut schemars::SchemaGenerator) -> Vec<crate::custom_requests::CustomMethodSchema> {
|
||||
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 })
|
||||
}
|
||||
|
||||
struct Route {
|
||||
method_name: String,
|
||||
fn_ident: syn::Ident,
|
||||
param_type: Option<Type>,
|
||||
#[allow(dead_code)]
|
||||
return_type: Option<Type>,
|
||||
ok_type: Option<Type>,
|
||||
}
|
||||
|
||||
/// Extract the type of the first non-self parameter, if any.
|
||||
fn extract_param_type(sig: &syn::Signature) -> Option<Type> {
|
||||
for input in &sig.inputs {
|
||||
if let FnArg::Typed(pat_type) = input {
|
||||
if let Pat::Ident(pat_ident) = &*pat_type.pat {
|
||||
if pat_ident.ident == "self" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Some((*pat_type.ty).clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract the full return type (e.g. `Result<T, E>`).
|
||||
fn extract_return_type(sig: &syn::Signature) -> Option<Type> {
|
||||
if let ReturnType::Type(_, ty) = &sig.output {
|
||||
Some((**ty).clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract `T` from `Result<T, E>` in the return type.
|
||||
fn extract_result_ok_type(sig: &syn::Signature) -> Option<Type> {
|
||||
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
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -33,13 +37,15 @@ 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"] }
|
||||
goose-acp-macros = { version = "1.24.0", path = "../goose-acp-macros" }
|
||||
|
||||
[dev-dependencies]
|
||||
assert-json-diff = "2.0.2"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +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 mut generator = SchemaGenerator::default();
|
||||
let methods = GooseAcpAgent::custom_method_schemas(&mut generator);
|
||||
|
||||
// Collect $defs from the generator (all types referenced via subschema_for).
|
||||
let mut defs: Map<String, Value> = 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<String, Vec<String>> = 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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build ExtRequest.params and ExtResponse.result anyOf arrays,
|
||||
// deduplicating response variants (e.g. EmptyResponse appears once).
|
||||
let mut request_variants: Vec<Value> = Vec::new();
|
||||
let mut response_variants: Vec<Value> = Vec::new();
|
||||
let mut seen_response_types: BTreeSet<String> = 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,
|
||||
}),
|
||||
);
|
||||
|
||||
// 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<Value> = 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}");
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
use schemars::JsonSchema;
|
||||
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<schemars::Schema>,
|
||||
pub params_type_name: Option<String>,
|
||||
pub response_schema: Option<schemars::Schema>,
|
||||
pub response_type_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Add an extension to an active session.
|
||||
#[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.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct RemoveExtensionRequest {
|
||||
pub session_id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// List all tools available in a session.
|
||||
#[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<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Read a resource from an extension.
|
||||
#[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,
|
||||
}
|
||||
|
||||
/// Update the working directory for a session.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct UpdateWorkingDirRequest {
|
||||
pub session_id: String,
|
||||
pub working_dir: String,
|
||||
}
|
||||
|
||||
/// Get a session by ID.
|
||||
#[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.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct ListSessionsResponse {
|
||||
pub sessions: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Delete a session.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct DeleteSessionRequest {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
/// Export a session as a JSON string.
|
||||
#[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.
|
||||
#[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,
|
||||
}
|
||||
|
||||
/// List configured extensions and any warnings.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct GetExtensionsResponse {
|
||||
/// Array of ExtensionEntry objects with `enabled` flag and config details.
|
||||
pub extensions: Vec<serde_json::Value>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Empty success response for operations that return no data.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct EmptyResponse {}
|
||||
@@ -1,6 +1,7 @@
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
mod adapters;
|
||||
pub mod custom_requests;
|
||||
pub mod server;
|
||||
pub mod server_factory;
|
||||
pub mod transport;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::custom_requests::*;
|
||||
use anyhow::Result;
|
||||
use fs_err as fs;
|
||||
use goose::agents::extension::{Envs, PLATFORM_EXTENSIONS};
|
||||
@@ -17,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,
|
||||
@@ -994,6 +996,192 @@ impl GooseAcpAgent {
|
||||
}
|
||||
}
|
||||
|
||||
#[custom_methods]
|
||||
impl GooseAcpAgent {
|
||||
#[custom_method("extensions/add")]
|
||||
async fn on_add_extension(
|
||||
&self,
|
||||
req: AddExtensionRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
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()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method("extensions/remove")]
|
||||
async fn on_remove_extension(
|
||||
&self,
|
||||
req: RemoveExtensionRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
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()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method("tools")]
|
||||
async fn on_get_tools(&self, req: GetToolsRequest) -> Result<GetToolsResponse, sacp::Error> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(GetToolsResponse { tools: tools_json })
|
||||
}
|
||||
|
||||
#[custom_method("resource/read")]
|
||||
async fn on_read_resource(
|
||||
&self,
|
||||
req: ReadResourceRequest,
|
||||
) -> Result<ReadResourceResponse, sacp::Error> {
|
||||
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()))?;
|
||||
Ok(ReadResourceResponse {
|
||||
result: result_json,
|
||||
})
|
||||
}
|
||||
|
||||
#[custom_method("working_dir/update")]
|
||||
async fn on_update_working_dir(
|
||||
&self,
|
||||
req: UpdateWorkingDirRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
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()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method("session/list")]
|
||||
async fn on_list_sessions(&self) -> Result<ListSessionsResponse, sacp::Error> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(ListSessionsResponse {
|
||||
sessions: sessions_json,
|
||||
})
|
||||
}
|
||||
|
||||
#[custom_method("session/get")]
|
||||
async fn on_get_session(
|
||||
&self,
|
||||
req: GetSessionRequest,
|
||||
) -> Result<GetSessionResponse, sacp::Error> {
|
||||
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()))?;
|
||||
Ok(GetSessionResponse {
|
||||
session: session_json,
|
||||
})
|
||||
}
|
||||
|
||||
#[custom_method("session/delete")]
|
||||
async fn on_delete_session(
|
||||
&self,
|
||||
req: DeleteSessionRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.session_manager
|
||||
.delete_session(&req.session_id)
|
||||
.await
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method("session/export")]
|
||||
async fn on_export_session(
|
||||
&self,
|
||||
req: ExportSessionRequest,
|
||||
) -> Result<ExportSessionResponse, sacp::Error> {
|
||||
let data = self
|
||||
.session_manager
|
||||
.export_session(&req.session_id)
|
||||
.await
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(ExportSessionResponse { data })
|
||||
}
|
||||
|
||||
#[custom_method("session/import")]
|
||||
async fn on_import_session(
|
||||
&self,
|
||||
req: ImportSessionRequest,
|
||||
) -> Result<ImportSessionResponse, sacp::Error> {
|
||||
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()))?;
|
||||
Ok(ImportSessionResponse {
|
||||
session: session_json,
|
||||
})
|
||||
}
|
||||
|
||||
#[custom_method("config/extensions")]
|
||||
async fn on_get_extensions(&self) -> Result<GetExtensionsResponse, sacp::Error> {
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(GetExtensionsResponse {
|
||||
extensions: extensions_json,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_agent_for_session(&self, session_id: &str) -> Result<Arc<Agent>, 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 {
|
||||
pub agent: Arc<GooseAcpAgent>,
|
||||
}
|
||||
@@ -1061,7 +1249,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
|
||||
// - _<method>: custom requests that will eventually route to goose-server
|
||||
.otherwise({
|
||||
let agent = self.agent.clone();
|
||||
|message: MessageCx| async move {
|
||||
@@ -1079,6 +1269,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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#[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<sacp::ClientToAgent>,
|
||||
method: &str,
|
||||
params: serde_json::Value,
|
||||
) -> Result<serde_json::Value, sacp::Error> {
|
||||
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(),
|
||||
"_goose/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(), "_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");
|
||||
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(),
|
||||
"_goose/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(),
|
||||
"_goose/session/delete",
|
||||
serde_json::json!({ "session_id": session_id }),
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_ok(), "delete failed: {:?}", result);
|
||||
|
||||
let result = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/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(),
|
||||
"_goose/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(), "_goose/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");
|
||||
});
|
||||
}
|
||||
+7
@@ -34,6 +34,13 @@ pub struct ClientToAgentSession {
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl ClientToAgentConnection {
|
||||
#[allow(dead_code)]
|
||||
pub fn cx(&self) -> &JrConnectionCx<ClientToAgent> {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Connection for ClientToAgentConnection {
|
||||
type Session = ClientToAgentSession;
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generates TypeScript types + Zod validators for Goose custom extension methods.
|
||||
*
|
||||
* Usage:
|
||||
* npm run generate # build Rust schema, then generate TS
|
||||
*/
|
||||
|
||||
import { createClient } from "@hey-api/openapi-ts";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs/promises";
|
||||
import { dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import * as prettier from "prettier";
|
||||
|
||||
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);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
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);
|
||||
|
||||
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"],
|
||||
});
|
||||
|
||||
await postProcessTypes();
|
||||
await postProcessIndex(meta);
|
||||
|
||||
await generateClient(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 "";
|
||||
});
|
||||
|
||||
// Fix bare relative imports to use .js extensions (required by nodenext consumers)
|
||||
src = fixRelativeImports(src);
|
||||
|
||||
// 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}`);
|
||||
|
||||
// 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<string>();
|
||||
const zodImports = new Set<string>();
|
||||
|
||||
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<string, unknown>";
|
||||
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<string, unknown>): Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
${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);
|
||||
}
|
||||
Generated
+1277
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "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",
|
||||
"lint": "tsc --noEmit",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"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",
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// This file is auto-generated — do not edit manually.
|
||||
|
||||
export interface ExtMethodProvider {
|
||||
extMethod(
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
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<void> {
|
||||
await this.conn.extMethod("_goose/extensions/add", params);
|
||||
}
|
||||
|
||||
async extensionsRemove(params: RemoveExtensionRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/extensions/remove", params);
|
||||
}
|
||||
|
||||
async tools(params: GetToolsRequest): Promise<GetToolsResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/tools", params);
|
||||
return zGetToolsResponse.parse(raw) as GetToolsResponse;
|
||||
}
|
||||
|
||||
async resourceRead(
|
||||
params: ReadResourceRequest,
|
||||
): Promise<ReadResourceResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/resource/read", params);
|
||||
return zReadResourceResponse.parse(raw) as ReadResourceResponse;
|
||||
}
|
||||
|
||||
async workingDirUpdate(params: UpdateWorkingDirRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/working_dir/update", params);
|
||||
}
|
||||
|
||||
async sessionList(): Promise<ListSessionsResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/session/list", {});
|
||||
return zListSessionsResponse.parse(raw) as ListSessionsResponse;
|
||||
}
|
||||
|
||||
async sessionGet(params: GetSessionRequest): Promise<GetSessionResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/session/get", params);
|
||||
return zGetSessionResponse.parse(raw) as GetSessionResponse;
|
||||
}
|
||||
|
||||
async sessionDelete(params: DeleteSessionRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/session/delete", params);
|
||||
}
|
||||
|
||||
async sessionExport(
|
||||
params: ExportSessionRequest,
|
||||
): Promise<ExportSessionResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/session/export", params);
|
||||
return zExportSessionResponse.parse(raw) as ExportSessionResponse;
|
||||
}
|
||||
|
||||
async sessionImport(
|
||||
params: ImportSessionRequest,
|
||||
): Promise<ImportSessionResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/session/import", params);
|
||||
return zImportSessionResponse.parse(raw) as ImportSessionResponse;
|
||||
}
|
||||
|
||||
async configExtensions(): Promise<GetExtensionsResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/config/extensions", {});
|
||||
return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse;
|
||||
}
|
||||
|
||||
async toolCall(): Promise<Record<string, unknown>> {
|
||||
return await this.conn.extMethod("_goose/tool/call", {});
|
||||
}
|
||||
|
||||
async providerUpdate(): Promise<Record<string, unknown>> {
|
||||
return await this.conn.extMethod("_goose/provider/update", {});
|
||||
}
|
||||
|
||||
async containerSet(): Promise<Record<string, unknown>> {
|
||||
return await this.conn.extMethod("_goose/container/set", {});
|
||||
}
|
||||
|
||||
async appsList(): Promise<Record<string, unknown>> {
|
||||
return await this.conn.extMethod("_goose/apps/list", {});
|
||||
}
|
||||
|
||||
async appsExport(): Promise<Record<string, unknown>> {
|
||||
return await this.conn.extMethod("_goose/apps/export", {});
|
||||
}
|
||||
|
||||
async appsImport(): Promise<Record<string, unknown>> {
|
||||
return await this.conn.extMethod("_goose/apps/import", {});
|
||||
}
|
||||
|
||||
async configProviders(): Promise<Record<string, unknown>> {
|
||||
return await this.conn.extMethod("_goose/config/providers", {});
|
||||
}
|
||||
}
|
||||
@@ -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.js';
|
||||
|
||||
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];
|
||||
@@ -0,0 +1,165 @@
|
||||
// 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<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<unknown>;
|
||||
warnings: Array<string>;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
// 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()
|
||||
})
|
||||
]);
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./generated/index.js";
|
||||
export * from "./generated/zod.gen.js";
|
||||
export { GooseClient } from "./generated/client.gen.js";
|
||||
@@ -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"]
|
||||
}
|
||||
Generated
+28
-32
@@ -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",
|
||||
@@ -219,7 +233,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 +602,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -630,7 +642,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -1088,7 +1099,6 @@
|
||||
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
@@ -2722,7 +2732,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 +3207,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 +6531,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 +6820,6 @@
|
||||
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -6853,7 +6861,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 +6871,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -7005,7 +7011,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 +7396,6 @@
|
||||
"integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.18",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -7640,7 +7644,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7713,7 +7716,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 +8256,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -9557,7 +9558,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 +9617,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^24.9.0",
|
||||
@@ -10623,7 +10624,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 +11107,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",
|
||||
@@ -11986,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",
|
||||
@@ -12337,7 +12340,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 +13396,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 +14638,7 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -14657,7 +14659,6 @@
|
||||
"integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
@@ -17033,7 +17034,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -17135,6 +17135,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 +17151,7 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -17505,7 +17507,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 +17516,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 +17537,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 +19455,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 +19986,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -20411,7 +20410,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -20502,7 +20500,6 @@
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
@@ -21151,7 +21148,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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user