diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34201a59..b8e7ab0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,6 +194,13 @@ jobs: source ./bin/activate-hermit just check-acp-schema + - name: Test ACP Client SDK + run: | + source ./bin/activate-hermit + cd ui/sdk + pnpm test + pnpm run typecheck:test + desktop-lint: name: Test and Lint Electron Desktop App runs-on: macos-latest diff --git a/crates/goose/src/acp/response_builder.rs b/crates/goose/src/acp/response_builder.rs index 7a93bce5..d108aa11 100644 --- a/crates/goose/src/acp/response_builder.rs +++ b/crates/goose/src/acp/response_builder.rs @@ -215,10 +215,13 @@ fn available_commands_update(working_dir: &std::path::Path) -> AvailableCommands pub(super) fn send_session_setup_notifications( cx: &ConnectionTo, session: &Session, + supports_goose_custom_notifications: bool, ) -> Result<(), agent_client_protocol::Error> { let session_id = SessionId::new(session.id.clone()); if let Some(updates) = build_usage_updates(session) { - cx.send_notification(updates.custom)?; + if supports_goose_custom_notifications { + cx.send_notification(updates.custom)?; + } cx.send_notification(SessionNotification::new( session_id.clone(), SessionUpdate::UsageUpdate(updates.standard), diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index d6bd5f7f..7cefd1e0 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -208,6 +208,7 @@ pub struct GooseAcpAgent { client_fs_capabilities: OnceCell, client_terminal: OnceCell, client_mcp_host_info: OnceCell, + client_supports_goose_custom_notifications: OnceCell, use_login_shell_path: OnceCell, client_cx: OnceCell>, config_dir: std::path::PathBuf, @@ -421,15 +422,17 @@ fn extract_timeout_from_meta(meta: &Option) -> Option { } #[derive(Debug, Default, Deserialize)] -struct GooseClientMetaEnvelope { +struct ClientCapabilitiesMeta { #[serde(default)] - goose: Option, + goose: Option, } #[derive(Debug, Default, Deserialize)] -struct GooseClientMeta { +struct GooseClientCapabilities { #[serde(rename = "mcpHostCapabilities", default)] mcp_host_capabilities: Option, + #[serde(rename = "customNotifications", default)] + custom_notifications: Option, } #[derive(Debug, Default, Deserialize)] @@ -438,24 +441,25 @@ struct GooseMcpHostCapabilities { extensions: Option, } -fn extract_goose_client_meta(meta: &Meta) -> Option { - serde_json::from_value(serde_json::Value::Object(meta.clone())).ok() -} - -fn extract_client_mcp_host_info(args: &InitializeRequest) -> GooseMcpHostInfo { - let host_capabilities = args - .client_capabilities +fn extract_client_capabilities_meta(args: &InitializeRequest) -> Option { + args.client_capabilities .meta .as_ref() - .and_then(extract_goose_client_meta) - .and_then(|meta| meta.goose) - .and_then(|goose| goose.mcp_host_capabilities); + .and_then(|meta| serde_json::from_value(serde_json::Value::Object(meta.clone())).ok()) +} + +fn extract_client_mcp_host_info( + args: &InitializeRequest, + goose_client_capabilities: Option<&GooseClientCapabilities>, +) -> GooseMcpHostInfo { + let host_capabilities = + goose_client_capabilities.and_then(|goose| goose.mcp_host_capabilities.as_ref()); let explicit_extensions = host_capabilities .as_ref() .and_then(|capabilities| capabilities.extensions.as_ref()) .is_some(); let extensions = host_capabilities - .and_then(|capabilities| capabilities.extensions) + .and_then(|capabilities| capabilities.extensions.clone()) .unwrap_or_default(); GooseMcpHostInfo { @@ -1002,6 +1006,13 @@ impl GooseAcpAgent { Arc::clone(&self.permission_manager) } + pub(super) fn supports_goose_custom_notifications(&self) -> bool { + self.client_supports_goose_custom_notifications + .get() + .copied() + .unwrap_or(false) + } + // TODO: goose reads Paths::in_state_dir globally (e.g. RequestLog), ignoring this data_dir. pub async fn new(options: GooseAcpAgentOptions) -> Result { let session_manager = Arc::new(SessionManager::new(options.data_dir)); @@ -1032,6 +1043,7 @@ impl GooseAcpAgent { client_fs_capabilities: OnceCell::new(), client_terminal: OnceCell::new(), client_mcp_host_info: OnceCell::new(), + client_supports_goose_custom_notifications: OnceCell::new(), use_login_shell_path: OnceCell::new(), client_cx: OnceCell::new(), config_dir: options.config_dir, @@ -1468,18 +1480,28 @@ impl GooseAcpAgent { } => { send_elicitation_interaction_update( cx, + self.supports_goose_custom_notifications(), session_id.0.as_ref(), - id.clone(), - InteractionState::Pending, - Some(message.clone()), - Some(requested_schema.clone()), - Some(interaction_update_meta(message_id, message_created)), + InteractionUpdate { + interaction: Interaction::Elicitation { + id: id.clone(), + state: InteractionState::Pending, + message: Some(message.clone()), + requested_schema: Some(requested_schema.clone()), + }, + meta: Some(interaction_update_meta(message_id, message_created)), + }, )?; } ActionRequiredData::ElicitationResponse { .. } => {} }, MessageContent::SystemNotification(notification) => { - send_status_message_update(cx, session_id.0.as_ref(), notification)?; + send_status_message_update( + cx, + self.supports_goose_custom_notifications(), + session_id.0.as_ref(), + notification, + )?; } _ => {} } @@ -1996,6 +2018,14 @@ impl GooseAcpAgent { } } +fn extract_client_supports_goose_custom_notifications( + goose_client_capabilities: Option<&GooseClientCapabilities>, +) -> bool { + goose_client_capabilities + .and_then(|goose| goose.custom_notifications) + .unwrap_or(false) +} + fn outcome_to_confirmation(outcome: &RequestPermissionOutcome) -> PermissionConfirmation { PermissionConfirmation { principal_type: PrincipalType::Tool, @@ -2043,14 +2073,17 @@ fn credits_exhausted_prompt_error( fn send_status_message_update( cx: &ConnectionTo, + supports_goose_custom_notifications: bool, session_id: &str, notification: &SystemNotificationContent, ) -> Result<(), agent_client_protocol::Error> { if let Some(status) = status_message_from_system_notification(notification) { - cx.send_notification(GooseSessionNotification { - session_id: session_id.to_string(), - update: GooseSessionUpdate::StatusMessage(StatusMessageUpdate { status }), - })?; + if supports_goose_custom_notifications { + cx.send_notification(GooseSessionNotification { + session_id: session_id.to_string(), + update: GooseSessionUpdate::StatusMessage(StatusMessageUpdate { status }), + })?; + } } Ok(()) } @@ -2071,25 +2104,17 @@ fn status_message_from_system_notification( fn send_elicitation_interaction_update( cx: &ConnectionTo, + supports_goose_custom_notifications: bool, session_id: &str, - id: String, - state: InteractionState, - message: Option, - requested_schema: Option, - meta: Option, + update: InteractionUpdate, ) -> Result<(), agent_client_protocol::Error> { - cx.send_notification(GooseSessionNotification { - session_id: session_id.to_string(), - update: GooseSessionUpdate::InteractionUpdate(InteractionUpdate { - interaction: Interaction::Elicitation { - id, - state, - message, - requested_schema, - }, - meta, - }), - }) + if supports_goose_custom_notifications { + cx.send_notification(GooseSessionNotification { + session_id: session_id.to_string(), + update: GooseSessionUpdate::InteractionUpdate(update), + })?; + } + Ok(()) } fn interaction_update_meta(message_id: Option<&str>, created: i64) -> serde_json::Value { @@ -2221,9 +2246,15 @@ impl GooseAcpAgent { .client_fs_capabilities .set(args.client_capabilities.fs.clone()); let _ = self.client_terminal.set(args.client_capabilities.terminal); - let _ = self - .client_mcp_host_info - .set(extract_client_mcp_host_info(&args)); + let goose_client_capabilities = + extract_client_capabilities_meta(&args).and_then(|meta| meta.goose); + let _ = self.client_mcp_host_info.set(extract_client_mcp_host_info( + &args, + goose_client_capabilities.as_ref(), + )); + let _ = self.client_supports_goose_custom_notifications.set( + extract_client_supports_goose_custom_notifications(goose_client_capabilities.as_ref()), + ); let _ = self .use_login_shell_path .set(extract_use_login_shell_path(&args)); @@ -2513,7 +2544,9 @@ impl GooseAcpAgent { .await .internal_err_ctx("Failed to load session")?; if let Some(updates) = build_usage_updates(&session) { - cx.send_notification(updates.custom)?; + if self.supports_goose_custom_notifications() { + cx.send_notification(updates.custom)?; + } // Standard ACP notification — emitted alongside the custom one for // backwards compatibility. Remove once all known clients have // migrated to `_goose/unstable/session/update`. @@ -2590,15 +2623,20 @@ impl GooseAcpAgent { send_elicitation_interaction_update( cx, + self.supports_goose_custom_notifications(), &req.session_id, - req.elicitation_id, - InteractionState::Submitted, - None, - None, - Some(interaction_update_meta( - response_message.id.as_deref(), - response_message.created, - )), + InteractionUpdate { + interaction: Interaction::Elicitation { + id: req.elicitation_id, + state: InteractionState::Submitted, + message: None, + requested_schema: None, + }, + meta: Some(interaction_update_meta( + response_message.id.as_deref(), + response_message.created, + )), + }, )?; Ok(EmptyResponse {}) @@ -3722,4 +3760,39 @@ print(\"hello, world\") let session = make_session_with_usage(Some(120), Some(80), Some(40), None, None, None); assert!(build_usage_updates(&session).is_none()); } + + #[test] + fn test_goose_custom_notifications_capability_defaults_to_false() { + let request = + InitializeRequest::new(agent_client_protocol::schema::ProtocolVersion::LATEST); + let goose_client_capabilities = + extract_client_capabilities_meta(&request).and_then(|meta| meta.goose); + + assert!(!extract_client_supports_goose_custom_notifications( + goose_client_capabilities.as_ref() + )); + } + + #[test] + fn test_goose_custom_notifications_capability_reads_client_meta() { + let mut goose_meta = serde_json::Map::new(); + goose_meta.insert( + "customNotifications".to_string(), + serde_json::Value::Bool(true), + ); + let mut meta = serde_json::Map::new(); + meta.insert("goose".to_string(), serde_json::Value::Object(goose_meta)); + + let request = + InitializeRequest::new(agent_client_protocol::schema::ProtocolVersion::LATEST) + .client_capabilities( + agent_client_protocol::schema::ClientCapabilities::new().meta(meta), + ); + let goose_client_capabilities = + extract_client_capabilities_meta(&request).and_then(|meta| meta.goose); + + assert!(extract_client_supports_goose_custom_notifications( + goose_client_capabilities.as_ref() + )); + } } diff --git a/crates/goose/src/acp/server/fork_session.rs b/crates/goose/src/acp/server/fork_session.rs index c0e3c764..976dd047 100644 --- a/crates/goose/src/acp/server/fork_session.rs +++ b/crates/goose/src/acp/server/fork_session.rs @@ -66,7 +66,11 @@ impl GooseAcpAgent { if let Some(co) = config_options { response = response.config_options(co); } - send_session_setup_notifications(cx, &goose_session)?; + send_session_setup_notifications( + cx, + &goose_session, + self.supports_goose_custom_notifications(), + )?; Ok(response) } } diff --git a/crates/goose/src/acp/server/load_session.rs b/crates/goose/src/acp/server/load_session.rs index 6a2fc862..94681a3b 100644 --- a/crates/goose/src/acp/server/load_session.rs +++ b/crates/goose/src/acp/server/load_session.rs @@ -29,6 +29,7 @@ fn send_replay_content_chunk( fn replay_conversation_to_client( cx: &ConnectionTo, session: &Session, + supports_goose_custom_notifications: bool, ) -> Result, agent_client_protocol::Error> { let session_id = SessionId::new(session.id.clone()); @@ -165,12 +166,19 @@ fn replay_conversation_to_client( if !submitted_elicitation_ids.contains(id) { send_elicitation_interaction_update( cx, + supports_goose_custom_notifications, session_id.0.as_ref(), - id.clone(), - InteractionState::Pending, - Some(elicitation_message.clone()), - Some(requested_schema.clone()), - Some(serde_json::Value::Object(replay_message_meta(message))), + InteractionUpdate { + interaction: Interaction::Elicitation { + id: id.clone(), + state: InteractionState::Pending, + message: Some(elicitation_message.clone()), + requested_schema: Some(requested_schema.clone()), + }, + meta: Some(serde_json::Value::Object(replay_message_meta( + message, + ))), + }, )?; } } @@ -226,7 +234,11 @@ impl GooseAcpAgent { .prepare_session_for_activation(session, args.cwd.clone(), args.mcp_servers, true) .await?; - let replay_tool_requests = replay_conversation_to_client(cx, &session)?; + let replay_tool_requests = replay_conversation_to_client( + cx, + &session, + self.supports_goose_custom_notifications(), + )?; let (agent, extension_results) = self.prepare_acp_session_agent(cx, &session).await?; self.register_acp_session(session_id_str.clone(), agent.clone(), replay_tool_requests) .await; @@ -245,7 +257,7 @@ impl GooseAcpAgent { let (mode_state, model_state, config_options) = build_session_setup_config(&self.provider_inventory, &session).await?; - send_session_setup_notifications(cx, &session)?; + send_session_setup_notifications(cx, &session, self.supports_goose_custom_notifications())?; let mut response = LoadSessionResponse::new().modes(mode_state); if let Some(ms) = model_state { diff --git a/crates/goose/src/acp/server/new_session.rs b/crates/goose/src/acp/server/new_session.rs index dba27c84..c1a6d5c5 100644 --- a/crates/goose/src/acp/server/new_session.rs +++ b/crates/goose/src/acp/server/new_session.rs @@ -96,7 +96,11 @@ impl GooseAcpAgent { meta.insert("extensionResults".to_string(), extension_results); response = response.meta(meta); } - super::send_session_setup_notifications(cx, &goose_session)?; + super::send_session_setup_notifications( + cx, + &goose_session, + self.supports_goose_custom_notifications(), + )?; debug!( target: "perf", sid = %sid, diff --git a/ui/desktop/src/acp/acpConnection.ts b/ui/desktop/src/acp/acpConnection.ts index 9cbdff89..59caba3b 100644 --- a/ui/desktop/src/acp/acpConnection.ts +++ b/ui/desktop/src/acp/acpConnection.ts @@ -2,7 +2,6 @@ import { DEFAULT_GOOSE_MCP_HOST_CAPABILITIES, GooseClient, type Client, - type GooseInitializeRequest, } from '@aaif/goose-sdk'; import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'; import packageJson from '../../package.json'; @@ -58,7 +57,7 @@ async function initializeConnection(): Promise { name: packageJson.name, version: packageJson.version, }, - } satisfies GooseInitializeRequest); + }); monitorConnection(client); return client; diff --git a/ui/sdk/generate-schema.ts b/ui/sdk/generate-schema.ts index 66d7ed67..8d67c5d1 100644 --- a/ui/sdk/generate-schema.ts +++ b/ui/sdk/generate-schema.ts @@ -239,18 +239,16 @@ async function generateClient(meta: { const handlerFields: string[] = []; const dispatchCases: string[] = []; - const handlerKeys: string[] = []; for (const n of meta.notifications ?? []) { const handlerName = methodToHandlerName(n.method); - handlerKeys.push(handlerName); if (!n.paramsType) { handlerFields.push( ` ${handlerName}?: (params: Record) => Promise;`, ); dispatchCases.push( ` case "${n.method}": { - await ${handlerName}?.(params); + await callbacks.${handlerName}?.(params); return; }`, ); @@ -265,16 +263,12 @@ async function generateClient(meta: { dispatchCases.push( ` case "${n.method}": { const parsed = ${zodName}.parse(params) as ${n.paramsType}; - await ${handlerName}?.(parsed); + await callbacks.${handlerName}?.(parsed); return; }`, ); } - const handlerDestructure = - handlerKeys.length > 0 - ? `const { ${handlerKeys.join(", ")}, ...rest } = callbacks;` - : `const rest = callbacks;`; const handlersInterface = `export interface GooseExtNotifications { ${handlerFields.join("\n")} }`; @@ -282,19 +276,26 @@ ${handlerFields.join("\n")} const dispatcherFn = `export function installGooseExtNotificationDispatcher( callbacks: GooseClientCallbacks, ): Client { - ${handlerDestructure} - const userExtNotification = rest.extNotification; - return { - ...rest, + const dispatcher: Pick = { extNotification: async (method, params) => { switch (method) { ${dispatchCases.join("\n")} default: - await userExtNotification?.(method, params); + await callbacks.extNotification?.(method, params); return; } }, }; + return new Proxy(callbacks, { + get(target, property) { + if (property === "extNotification") { + return dispatcher.extNotification; + } + + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }) as Client; }`; const upstreamImportLine = `import type { ${[...upstreamTypeImports].sort().join(", ")} } from "@agentclientprotocol/sdk";`; @@ -322,7 +323,10 @@ ${methodDefs.join("\n")} ${handlersInterface} -export type GooseClientCallbacks = Client & GooseExtNotifications; +export type GooseClientCallbacks = + Omit & + Partial> & + GooseExtNotifications; ${dispatcherFn} `; diff --git a/ui/sdk/package.json b/ui/sdk/package.json index eecc08ef..882e23d8 100644 --- a/ui/sdk/package.json +++ b/ui/sdk/package.json @@ -37,6 +37,8 @@ "build:native:all": "tsx scripts/build-native.ts --all", "generate": "tsx generate-schema.ts", "lint": "tsc --noEmit", + "test": "node --import tsx --test tests/*.test.ts", + "typecheck:test": "tsc -p tsconfig.test.json --noEmit", "format": "prettier --write src/", "check:compat": "node scripts/check-binary-compat.mjs" }, diff --git a/ui/sdk/src/client-capabilities.ts b/ui/sdk/src/client-capabilities.ts new file mode 100644 index 00000000..8ae2af3b --- /dev/null +++ b/ui/sdk/src/client-capabilities.ts @@ -0,0 +1,8 @@ +import type { GooseMcpHostCapabilities } from "./mcp-apps.js"; + +export interface GooseClientCapabilitiesMeta { + goose?: { + mcpHostCapabilities?: GooseMcpHostCapabilities; + customNotifications?: boolean; + }; +} diff --git a/ui/sdk/src/generated/client.gen.ts b/ui/sdk/src/generated/client.gen.ts index 0d332565..9467dd0d 100644 --- a/ui/sdk/src/generated/client.gen.ts +++ b/ui/sdk/src/generated/client.gen.ts @@ -733,28 +733,37 @@ export interface GooseExtNotifications { ) => Promise; } -export type GooseClientCallbacks = Client & GooseExtNotifications; +export type GooseClientCallbacks = Omit & + Partial> & + GooseExtNotifications; export function installGooseExtNotificationDispatcher( callbacks: GooseClientCallbacks, ): Client { - const { unstable_sessionUpdate, ...rest } = callbacks; - const userExtNotification = rest.extNotification; - return { - ...rest, + const dispatcher: Pick = { extNotification: async (method, params) => { switch (method) { case "_goose/unstable/session/update": { const parsed = zGooseSessionNotification_unstable.parse( params, ) as GooseSessionNotification_unstable; - await unstable_sessionUpdate?.(parsed); + await callbacks.unstable_sessionUpdate?.(parsed); return; } default: - await userExtNotification?.(method, params); + await callbacks.extNotification?.(method, params); return; } }, }; + return new Proxy(callbacks, { + get(target, property) { + if (property === "extNotification") { + return dispatcher.extNotification; + } + + const value = Reflect.get(target, property, target); + return typeof value === "function" ? value.bind(target) : value; + }, + }) as Client; } diff --git a/ui/sdk/src/index.ts b/ui/sdk/src/index.ts index aa4cbe96..2d6815ad 100644 --- a/ui/sdk/src/index.ts +++ b/ui/sdk/src/index.ts @@ -6,6 +6,7 @@ export { } from "./generated/client.gen.js"; export { GooseClient } from "./goose-client.js"; export { createHttpStream } from "./http-stream.js"; +export * from "./client-capabilities.js"; export * from "./mcp-apps.js"; export { diff --git a/ui/sdk/src/mcp-apps.ts b/ui/sdk/src/mcp-apps.ts index 03f3be6e..5abbd038 100644 --- a/ui/sdk/src/mcp-apps.ts +++ b/ui/sdk/src/mcp-apps.ts @@ -1,7 +1,3 @@ -import type { - Implementation, - InitializeRequest, -} from "@agentclientprotocol/sdk"; import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge"; import type { McpUiAppResourceConfig, @@ -68,19 +64,6 @@ export interface GooseToolCallUpdateMeta { [key: string]: unknown; } -export interface GooseClientMeta { - goose: { - mcpHostCapabilities: GooseMcpHostCapabilities; - }; -} - -export type GooseInitializeRequest = InitializeRequest & { - clientCapabilities: NonNullable & { - _meta: GooseClientMeta; - }; - clientInfo: Implementation; -}; - export const DEFAULT_GOOSE_MCP_HOST_CAPABILITIES: GooseMcpHostCapabilities = { extensions: { [GOOSE_MCP_UI_EXTENSION_ID]: { diff --git a/ui/sdk/tests/client-callbacks.test.ts b/ui/sdk/tests/client-callbacks.test.ts new file mode 100644 index 00000000..e2b4bdc0 --- /dev/null +++ b/ui/sdk/tests/client-callbacks.test.ts @@ -0,0 +1,85 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { installGooseExtNotificationDispatcher } from "../src/generated/client.gen.ts"; +import type { GooseSessionNotification_unstable } from "../src/generated/types.gen.ts"; +import type { + RequestPermissionRequest, + RequestPermissionResponse, + SessionNotification, +} from "@agentclientprotocol/sdk"; + +class ClassBackedCallbacks { + #events: string[] = []; + + get events(): string[] { + return this.#events; + } + + async requestPermission( + _params: RequestPermissionRequest, + ): Promise { + this.#events.push("requestPermission"); + return { outcome: { outcome: "cancelled" } }; + } + + async sessionUpdate(_params: SessionNotification): Promise { + this.#events.push("sessionUpdate"); + } + + async extNotification( + method: string, + _params: Record, + ): Promise { + this.#events.push(`extNotification:${method}`); + } + + async unstable_sessionUpdate( + notification: GooseSessionNotification_unstable, + ): Promise { + this.#events.push( + `unstable_sessionUpdate:${notification.update.sessionUpdate}`, + ); + } +} + +class MinimalCallbacks { + async requestPermission( + _params: RequestPermissionRequest, + ): Promise { + return { outcome: { outcome: "cancelled" } }; + } + + async sessionUpdate(_params: SessionNotification): Promise {} +} + +test("dispatcher preserves class-backed callback receivers", async () => { + const callbacks = new ClassBackedCallbacks(); + const client = installGooseExtNotificationDispatcher(callbacks); + + await client.requestPermission({} as RequestPermissionRequest); + await client.sessionUpdate({} as SessionNotification); + await client.extNotification!("_goose/unstable/session/update", { + sessionId: "session-1", + update: { + sessionUpdate: "status_message", + status: { + type: "notice", + message: "ready", + }, + }, + }); + await client.extNotification!("example/unknown", {}); + + assert.deepEqual(callbacks.events, [ + "requestPermission", + "sessionUpdate", + "unstable_sessionUpdate:status_message", + "extNotification:example/unknown", + ]); +}); + +test("raw extNotification is optional", async () => { + const client = installGooseExtNotificationDispatcher(new MinimalCallbacks()); + + await client.extNotification!("example/unknown", {}); +}); diff --git a/ui/sdk/tsconfig.test.json b/ui/sdk/tsconfig.test.json new file mode 100644 index 00000000..b605b763 --- /dev/null +++ b/ui/sdk/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "noEmit": true, + "rootDir": "." + }, + "include": ["src", "tests"] +}