diff --git a/.caplets/.gitignore b/.caplets/.gitignore index d6b7ef32..1d9df9a6 100644 --- a/.caplets/.gitignore +++ b/.caplets/.gitignore @@ -1,2 +1,3 @@ * !.gitignore +!config.json diff --git a/.caplets/config.json b/.caplets/config.json new file mode 100644 index 00000000..f670cef7 --- /dev/null +++ b/.caplets/config.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://caplets.dev/config.schema.json", + "mcpServers": {} +} diff --git a/.changeset/hip-tigers-sing.md b/.changeset/hip-tigers-sing.md new file mode 100644 index 00000000..d977b657 --- /dev/null +++ b/.changeset/hip-tigers-sing.md @@ -0,0 +1,8 @@ +--- +"@caplets/core": minor +"@caplets/opencode": patch +"caplets": patch +"@caplets/pi": patch +--- + +Bump dependencies diff --git a/.changeset/published-schema-urls.md b/.changeset/published-schema-urls.md new file mode 100644 index 00000000..a8b7c27a --- /dev/null +++ b/.changeset/published-schema-urls.md @@ -0,0 +1,6 @@ +--- +"@caplets/core": patch +"caplets": patch +--- + +Publish the generated config and Caplet frontmatter schemas from caplets.dev and use those URLs in generated starter files. diff --git a/.changeset/versioned-attach-api.md b/.changeset/versioned-attach-api.md new file mode 100644 index 00000000..0132f389 --- /dev/null +++ b/.changeset/versioned-attach-api.md @@ -0,0 +1,5 @@ +--- +"@caplets/core": minor +--- + +Replace unversioned self-hosted HTTP routes with the `/v1` service surface and add the Caplets attach manifest/invoke API for native and attach clients. diff --git a/.codex/config.toml b/.codex/config.toml index a3721b56..01ce6b20 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -1,7 +1,16 @@ +#:schema https://raw.githubusercontent.com/openai/codex/refs/heads/main/codex-rs/core/config.schema.json + +[mcp_servers.caplets-remote] +command = "node" +args = ["./packages/cli/dist/index.js", "attach", "--remote-url", "http://localhost:5387"] + +[mcp_servers.caplets-remote.env] +CAPLETS_MODE = "remote" + [mcp_servers.caplets-local] command = "node" args = ["./packages/cli/dist/index.js", "serve", "--transport", "stdio"] -env_vars = ["GH_TOKEN"] [mcp_servers.caplets-local.env] CAPLETS_MODE = "local" +CAPLETS_CONFIG = "./.caplets/config.json" diff --git a/apps/landing/package.json b/apps/landing/package.json index 97255044..505fb306 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -14,9 +14,9 @@ "dependencies": { "@astrojs/check": "^0.9.9", "@hugeicons/core-free-icons": "4.2.0", - "@tailwindcss/vite": "^4.3.0", - "astro": "^6.4.3", - "tailwindcss": "^4.3.0", + "@tailwindcss/vite": "^4.3.1", + "astro": "^6.4.6", + "tailwindcss": "^4.3.1", "three": "^0.184.0", "typescript": "^6.0.3" }, diff --git a/apps/landing/public/caplet-frontmatter.schema.json b/apps/landing/public/caplet-frontmatter.schema.json new file mode 100644 index 00000000..aab59d71 --- /dev/null +++ b/apps/landing/public/caplet-frontmatter.schema.json @@ -0,0 +1,1607 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://caplets.dev/caplet-frontmatter.schema.json", + "title": "Caplet file frontmatter", + "description": "YAML frontmatter schema for a Markdown Caplet file.", + "type": "object", + "properties": { + "$schema": { + "description": "Optional JSON Schema URL for editor validation.", + "type": "string", + "format": "uri" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable Caplet display name." + }, + "description": { + "type": "string", + "description": "Compact capability description shown before the full Caplet card is disclosed." + }, + "tags": { + "description": "Optional tags for grouping or searching Caplets.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "exposure": { + "type": "string", + "enum": [ + "direct", + "progressive", + "code_mode", + "direct_and_code_mode", + "progressive_and_code_mode" + ], + "description": "How this Caplet is exposed to agents." + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "setup": { + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + }, + "verify": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "description": "Optional explicit setup and verification metadata for this Caplet." + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + } + }, + "resources": { + "type": "object", + "properties": { + "class": { + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + }, + "mcpServer": { + "type": "object", + "properties": { + "transport": { + "description": "Downstream MCP transport. Defaults to stdio when command is present.", + "type": "string", + "enum": ["stdio", "http", "sse"] + }, + "command": { + "description": "Executable command for stdio servers.", + "type": "string", + "minLength": 1 + }, + "args": { + "description": "Arguments passed to the stdio command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Environment variables for stdio servers. Supports ${VAR} and $env:VAR.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for stdio servers.", + "type": "string", + "minLength": 1 + }, + "url": { + "description": "Remote MCP server URL for http or sse transport.", + "type": "string", + "minLength": 1 + }, + "auth": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "none" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bearer" + }, + "token": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "token"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "headers" + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "headers"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth2" + }, + "authorizationUrl": { + "type": "string", + "minLength": 1 + }, + "tokenUrl": { + "type": "string", + "minLength": 1 + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "resourceMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "authorizationServerMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "openidConfigurationUrl": { + "type": "string", + "minLength": 1 + }, + "clientMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oidc" + }, + "authorizationUrl": { + "type": "string", + "minLength": 1 + }, + "tokenUrl": { + "type": "string", + "minLength": 1 + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "resourceMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "authorizationServerMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "openidConfigurationUrl": { + "type": "string", + "minLength": 1 + }, + "clientMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type"], + "additionalProperties": false + } + ], + "description": "Authentication settings for a remote MCP server." + }, + "startupTimeoutMs": { + "description": "Timeout in milliseconds for starting or checking a downstream server.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "callTimeoutMs": { + "description": "Timeout in milliseconds for downstream tool calls.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "toolCacheTtlMs": { + "description": "Milliseconds downstream tool metadata stays fresh. Set 0 to refresh every time.", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "description": "When true, omit this Caplet from discovery and do not start its MCP server.", + "type": "boolean" + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + } + }, + "resources": { + "type": "object", + "properties": { + "class": { + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + } + }, + "additionalProperties": false, + "description": "MCP server backend configuration for this Caplet." + }, + "openapiEndpoint": { + "type": "object", + "properties": { + "specPath": { + "description": "Local OpenAPI specification path.", + "type": "string", + "minLength": 1 + }, + "specUrl": { + "description": "Remote OpenAPI specification URL.", + "type": "string", + "minLength": 1 + }, + "baseUrl": { + "description": "Override base URL for OpenAPI requests.", + "type": "string", + "minLength": 1 + }, + "auth": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "none" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bearer" + }, + "token": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "token"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "headers" + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "headers"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth2" + }, + "authorizationUrl": { + "type": "string", + "minLength": 1 + }, + "tokenUrl": { + "type": "string", + "minLength": 1 + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "resourceMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "authorizationServerMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "openidConfigurationUrl": { + "type": "string", + "minLength": 1 + }, + "clientMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oidc" + }, + "authorizationUrl": { + "type": "string", + "minLength": 1 + }, + "tokenUrl": { + "type": "string", + "minLength": 1 + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "resourceMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "authorizationServerMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "openidConfigurationUrl": { + "type": "string", + "minLength": 1 + }, + "clientMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type"], + "additionalProperties": false + } + ], + "description": "Explicit OpenAPI request auth config. Use {\"type\":\"none\"} for public APIs." + }, + "requestTimeoutMs": { + "description": "Timeout in milliseconds for OpenAPI HTTP requests.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "operationCacheTtlMs": { + "description": "Milliseconds OpenAPI operation metadata stays fresh. Set 0 to refresh every time.", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "description": "When true, omit this Caplet from discovery.", + "type": "boolean" + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + } + }, + "resources": { + "type": "object", + "properties": { + "class": { + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + } + }, + "required": ["auth"], + "additionalProperties": false, + "description": "OpenAPI endpoint backend configuration for this Caplet." + }, + "graphqlEndpoint": { + "type": "object", + "properties": { + "endpointUrl": { + "type": "string", + "minLength": 1, + "description": "GraphQL HTTP endpoint URL." + }, + "schemaPath": { + "description": "Local GraphQL SDL or introspection path.", + "type": "string", + "minLength": 1 + }, + "schemaUrl": { + "description": "Remote GraphQL SDL or introspection URL.", + "type": "string", + "minLength": 1 + }, + "introspection": { + "description": "Load schema through endpoint introspection.", + "type": "boolean", + "const": true + }, + "operations": { + "description": "Configured GraphQL operations keyed by stable tool name.", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "document": { + "description": "Inline GraphQL operation document.", + "type": "string", + "minLength": 1 + }, + "documentPath": { + "description": "Path to a GraphQL operation document.", + "type": "string", + "minLength": 1 + }, + "operationName": { + "description": "Operation name to execute.", + "type": "string", + "minLength": 1 + }, + "description": { + "description": "Operation capability description.", + "type": "string", + "minLength": 1 + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + } + }, + "additionalProperties": false + } + }, + "auth": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "none" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bearer" + }, + "token": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "token"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "headers" + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "headers"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth2" + }, + "authorizationUrl": { + "type": "string", + "minLength": 1 + }, + "tokenUrl": { + "type": "string", + "minLength": 1 + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "resourceMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "authorizationServerMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "openidConfigurationUrl": { + "type": "string", + "minLength": 1 + }, + "clientMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oidc" + }, + "authorizationUrl": { + "type": "string", + "minLength": 1 + }, + "tokenUrl": { + "type": "string", + "minLength": 1 + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "resourceMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "authorizationServerMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "openidConfigurationUrl": { + "type": "string", + "minLength": 1 + }, + "clientMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type"], + "additionalProperties": false + } + ], + "description": "Explicit GraphQL request auth config. Use {\"type\":\"none\"} for public APIs." + }, + "requestTimeoutMs": { + "description": "Timeout in milliseconds for GraphQL HTTP requests.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "operationCacheTtlMs": { + "description": "Milliseconds GraphQL operation metadata stays fresh. Set 0 to refresh every time.", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "selectionDepth": { + "description": "Maximum depth for auto-generated GraphQL selection sets.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 5 + }, + "disabled": { + "description": "When true, omit this Caplet from discovery.", + "type": "boolean" + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + } + }, + "resources": { + "type": "object", + "properties": { + "class": { + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + } + }, + "required": ["endpointUrl", "auth"], + "additionalProperties": false, + "description": "GraphQL endpoint backend configuration for this Caplet." + }, + "httpApi": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "minLength": 1, + "pattern": "^(?![a-zA-Z][a-zA-Z0-9+.-]*:\\/\\/[^/?#]*@)[^?#]*$", + "description": "Base URL for HTTP action requests.", + "format": "uri" + }, + "auth": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "none" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bearer" + }, + "token": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "token"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "headers" + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "headers"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth2" + }, + "authorizationUrl": { + "type": "string", + "minLength": 1 + }, + "tokenUrl": { + "type": "string", + "minLength": 1 + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "resourceMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "authorizationServerMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "openidConfigurationUrl": { + "type": "string", + "minLength": 1 + }, + "clientMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oidc" + }, + "authorizationUrl": { + "type": "string", + "minLength": 1 + }, + "tokenUrl": { + "type": "string", + "minLength": 1 + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "resourceMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "authorizationServerMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "openidConfigurationUrl": { + "type": "string", + "minLength": 1 + }, + "clientMetadataUrl": { + "type": "string", + "minLength": 1 + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type"], + "additionalProperties": false + } + ], + "description": "Explicit HTTP API request auth config. Use {\"type\":\"none\"} for public APIs." + }, + "actions": { + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"], + "description": "HTTP method used for this action." + }, + "path": { + "type": "string", + "minLength": 1, + "pattern": "^\\/", + "description": "URL path appended to the HTTP API baseUrl." + }, + "description": { + "description": "Action capability description.", + "type": "string", + "minLength": 1 + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "inputSchema": { + "description": "JSON Schema for call_tool arguments.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "query": { + "description": "Query parameter mapping.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "headers": { + "description": "Request header mapping.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "jsonBody": { + "description": "JSON request body mapping." + } + }, + "required": ["method", "path"], + "additionalProperties": false + }, + "description": "Configured HTTP actions keyed by stable tool name.", + "minProperties": 1 + }, + "requestTimeoutMs": { + "description": "Timeout in milliseconds for HTTP action requests.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxResponseBytes": { + "description": "Maximum HTTP action response body bytes to read.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "description": "When true, omit this Caplet from discovery.", + "type": "boolean" + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + } + }, + "resources": { + "type": "object", + "properties": { + "class": { + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + } + }, + "required": ["baseUrl", "auth", "actions"], + "additionalProperties": false, + "description": "HTTP API backend configuration for this Caplet." + }, + "cliTools": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "description": { + "description": "Action capability description.", + "type": "string", + "minLength": 1 + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "inputSchema": { + "description": "JSON Schema for call_tool arguments.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "outputSchema": { + "description": "JSON Schema for structuredContent returned by this action.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this action.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "output": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text", "json"] + } + }, + "additionalProperties": false + }, + "annotations": { + "type": "object", + "properties": { + "readOnlyHint": { + "type": "boolean" + }, + "destructiveHint": { + "type": "boolean" + }, + "idempotentHint": { + "type": "boolean" + }, + "openWorldHint": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": ["command"], + "additionalProperties": false + }, + "description": "Configured CLI actions keyed by stable tool name.", + "minProperties": 1 + }, + "cwd": { + "description": "Default working directory for CLI actions.", + "type": "string", + "minLength": 1 + }, + "env": { + "description": "Default environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "description": "When true, omit this Caplet from discovery.", + "type": "boolean" + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + } + }, + "resources": { + "type": "object", + "properties": { + "class": { + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + } + }, + "required": ["actions"], + "additionalProperties": false, + "description": "CLI tools backend configuration for this Caplet." + }, + "capletSet": { + "type": "object", + "properties": { + "configPath": { + "description": "Child Caplets config.json path.", + "type": "string", + "minLength": 1 + }, + "capletsRoot": { + "description": "Child Markdown Caplets root directory.", + "type": "string", + "minLength": 1 + }, + "defaultSearchLimit": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxSearchLimit": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 50 + }, + "toolCacheTtlMs": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "description": "When true, omit this Caplet from discovery.", + "type": "boolean" + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + } + }, + "resources": { + "type": "object", + "properties": { + "class": { + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + } + }, + "additionalProperties": false, + "description": "Nested Caplet collection backend configuration for this Caplet." + } + }, + "required": ["name", "description"], + "additionalProperties": false +} diff --git a/apps/landing/public/config.schema.json b/apps/landing/public/config.schema.json new file mode 100644 index 00000000..9cd1369a --- /dev/null +++ b/apps/landing/public/config.schema.json @@ -0,0 +1,2527 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://caplets.dev/config.schema.json", + "title": "Caplets config", + "description": "Configuration file for the Caplets progressive MCP disclosure gateway.", + "type": "object", + "properties": { + "$schema": { + "description": "Optional JSON Schema URL for editor validation.", + "type": "string", + "format": "uri" + }, + "version": { + "default": 1, + "description": "Caplets config schema version.", + "type": "number", + "const": 1 + }, + "defaultSearchLimit": { + "default": 20, + "description": "Default maximum number of same-server search results.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxSearchLimit": { + "default": 50, + "description": "Maximum accepted search_tools limit.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 50 + }, + "completion": { + "default": { + "discoveryTimeoutMs": 750, + "overallTimeoutMs": 1500, + "cacheTtlMs": 300000, + "negativeCacheTtlMs": 30000 + }, + "description": "Shell completion discovery timeout and cache settings.", + "type": "object", + "properties": { + "discoveryTimeoutMs": { + "default": 750, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "overallTimeoutMs": { + "default": 1500, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "cacheTtlMs": { + "default": 300000, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "negativeCacheTtlMs": { + "default": 30000, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "additionalProperties": false + }, + "options": { + "default": { + "exposure": "code_mode", + "exposureDiscoveryTimeoutMs": 15000, + "exposureDiscoveryConcurrency": 4 + }, + "description": "Global Caplets runtime options.", + "type": "object", + "properties": { + "exposure": { + "default": "code_mode", + "type": "string", + "enum": [ + "direct", + "progressive", + "code_mode", + "direct_and_code_mode", + "progressive_and_code_mode" + ], + "description": "How this Caplet is exposed to agents." + }, + "exposureDiscoveryTimeoutMs": { + "default": 15000, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "exposureDiscoveryConcurrency": { + "default": 4, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 32 + } + }, + "additionalProperties": false + }, + "mcpServers": { + "default": {}, + "description": "Downstream MCP servers keyed by stable server ID.", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable server display name." + }, + "description": { + "type": "string", + "description": "Capability description shown to agents before downstream tools are disclosed." + }, + "transport": { + "description": "Downstream MCP transport. Defaults to stdio when command is present.", + "type": "string", + "enum": ["stdio", "http", "sse"] + }, + "command": { + "description": "Executable command for stdio servers.", + "type": "string", + "minLength": 1 + }, + "args": { + "description": "Arguments passed to the stdio command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Environment variables for stdio servers. Supports ${VAR} and $env:VAR.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for stdio servers.", + "type": "string", + "minLength": 1 + }, + "url": { + "description": "Remote MCP server URL for http or sse transport.", + "type": "string", + "format": "uri" + }, + "auth": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "none" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bearer" + }, + "token": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "token"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "headers" + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "headers"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth2" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "issuer": { + "type": "string", + "format": "uri" + }, + "resourceMetadataUrl": { + "type": "string", + "format": "uri" + }, + "authorizationServerMetadataUrl": { + "type": "string", + "format": "uri" + }, + "openidConfigurationUrl": { + "type": "string", + "format": "uri" + }, + "clientMetadataUrl": { + "type": "string", + "format": "uri" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oidc" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "issuer": { + "type": "string", + "format": "uri" + }, + "resourceMetadataUrl": { + "type": "string", + "format": "uri" + }, + "authorizationServerMetadataUrl": { + "type": "string", + "format": "uri" + }, + "openidConfigurationUrl": { + "type": "string", + "format": "uri" + }, + "clientMetadataUrl": { + "type": "string", + "format": "uri" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false + } + ], + "description": "Authentication settings for a remote MCP server." + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "exposure": { + "type": "string", + "enum": [ + "direct", + "progressive", + "code_mode", + "direct_and_code_mode", + "progressive_and_code_mode" + ], + "description": "How this Caplet is exposed to agents." + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "setup": { + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + }, + "verify": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true, + "description": "Requires Project Binding before this Caplet can run." + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + }, + "description": "Runtime features required by this Caplet." + }, + "resources": { + "description": "Hosted sandbox resource requirements.", + "type": "object", + "properties": { + "class": { + "description": "Requested hosted sandbox resource class.", + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + }, + "startupTimeoutMs": { + "default": 10000, + "description": "Timeout in milliseconds for starting or checking a downstream server.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "callTimeoutMs": { + "default": 60000, + "description": "Timeout in milliseconds for downstream tool calls.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "toolCacheTtlMs": { + "default": 30000, + "description": "Milliseconds downstream tool metadata stays fresh. Set 0 to refresh every time.", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "default": false, + "description": "When true, omit this server from Caplets discovery and do not start it.", + "type": "boolean" + } + }, + "required": ["name", "description"], + "additionalProperties": false + } + }, + "openapiEndpoints": { + "default": {}, + "description": "OpenAPI endpoints keyed by stable Caplet ID.", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable OpenAPI display name." + }, + "description": { + "type": "string", + "description": "Capability description shown to agents before OpenAPI operations are disclosed." + }, + "specPath": { + "description": "Local OpenAPI specification path.", + "type": "string", + "minLength": 1 + }, + "specUrl": { + "description": "Remote OpenAPI specification URL.", + "type": "string", + "format": "uri" + }, + "baseUrl": { + "description": "Override base URL for OpenAPI requests.", + "type": "string", + "format": "uri" + }, + "auth": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "none" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bearer" + }, + "token": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "token"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "headers" + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "headers"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth2" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "issuer": { + "type": "string", + "format": "uri" + }, + "resourceMetadataUrl": { + "type": "string", + "format": "uri" + }, + "authorizationServerMetadataUrl": { + "type": "string", + "format": "uri" + }, + "openidConfigurationUrl": { + "type": "string", + "format": "uri" + }, + "clientMetadataUrl": { + "type": "string", + "format": "uri" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oidc" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "issuer": { + "type": "string", + "format": "uri" + }, + "resourceMetadataUrl": { + "type": "string", + "format": "uri" + }, + "authorizationServerMetadataUrl": { + "type": "string", + "format": "uri" + }, + "openidConfigurationUrl": { + "type": "string", + "format": "uri" + }, + "clientMetadataUrl": { + "type": "string", + "format": "uri" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false + } + ], + "description": "Explicit OpenAPI request auth config. Use {\"type\":\"none\"} for public APIs." + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "exposure": { + "type": "string", + "enum": [ + "direct", + "progressive", + "code_mode", + "direct_and_code_mode", + "progressive_and_code_mode" + ], + "description": "How this Caplet is exposed to agents." + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "setup": { + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + }, + "verify": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true, + "description": "Requires Project Binding before this Caplet can run." + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + }, + "description": "Runtime features required by this Caplet." + }, + "resources": { + "description": "Hosted sandbox resource requirements.", + "type": "object", + "properties": { + "class": { + "description": "Requested hosted sandbox resource class.", + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + }, + "requestTimeoutMs": { + "default": 60000, + "description": "Timeout in milliseconds for OpenAPI HTTP requests.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "operationCacheTtlMs": { + "default": 30000, + "description": "Milliseconds OpenAPI operation metadata stays fresh. Set 0 to refresh every time.", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "default": false, + "description": "When true, omit this OpenAPI Caplet from discovery.", + "type": "boolean" + } + }, + "required": ["name", "description", "auth"], + "additionalProperties": false + } + }, + "graphqlEndpoints": { + "default": {}, + "description": "GraphQL endpoints keyed by stable Caplet ID.", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable GraphQL display name." + }, + "description": { + "type": "string", + "description": "Capability description shown to agents before GraphQL operations are disclosed." + }, + "endpointUrl": { + "type": "string", + "format": "uri", + "description": "GraphQL HTTP endpoint URL." + }, + "schemaPath": { + "description": "Local GraphQL SDL or introspection path.", + "type": "string", + "minLength": 1 + }, + "schemaUrl": { + "description": "Remote GraphQL SDL or introspection URL.", + "type": "string", + "format": "uri" + }, + "introspection": { + "description": "Load schema through endpoint introspection.", + "type": "boolean", + "const": true + }, + "operations": { + "description": "Configured GraphQL operations keyed by stable tool name.", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "document": { + "description": "Inline GraphQL operation document.", + "type": "string", + "minLength": 1 + }, + "documentPath": { + "description": "Path to a GraphQL operation document.", + "type": "string", + "minLength": 1 + }, + "operationName": { + "description": "Operation name to execute.", + "type": "string", + "minLength": 1 + }, + "description": { + "description": "Operation capability description.", + "type": "string", + "minLength": 1 + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + } + }, + "additionalProperties": false + } + }, + "auth": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "none" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bearer" + }, + "token": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "token"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "headers" + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "headers"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth2" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "issuer": { + "type": "string", + "format": "uri" + }, + "resourceMetadataUrl": { + "type": "string", + "format": "uri" + }, + "authorizationServerMetadataUrl": { + "type": "string", + "format": "uri" + }, + "openidConfigurationUrl": { + "type": "string", + "format": "uri" + }, + "clientMetadataUrl": { + "type": "string", + "format": "uri" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oidc" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "issuer": { + "type": "string", + "format": "uri" + }, + "resourceMetadataUrl": { + "type": "string", + "format": "uri" + }, + "authorizationServerMetadataUrl": { + "type": "string", + "format": "uri" + }, + "openidConfigurationUrl": { + "type": "string", + "format": "uri" + }, + "clientMetadataUrl": { + "type": "string", + "format": "uri" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false + } + ], + "description": "Explicit GraphQL request auth config. Use {\"type\":\"none\"} for public APIs." + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "exposure": { + "type": "string", + "enum": [ + "direct", + "progressive", + "code_mode", + "direct_and_code_mode", + "progressive_and_code_mode" + ], + "description": "How this Caplet is exposed to agents." + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "setup": { + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + }, + "verify": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true, + "description": "Requires Project Binding before this Caplet can run." + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + }, + "description": "Runtime features required by this Caplet." + }, + "resources": { + "description": "Hosted sandbox resource requirements.", + "type": "object", + "properties": { + "class": { + "description": "Requested hosted sandbox resource class.", + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + }, + "requestTimeoutMs": { + "default": 60000, + "description": "Timeout in milliseconds for GraphQL HTTP requests.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "operationCacheTtlMs": { + "default": 30000, + "description": "Milliseconds GraphQL operation metadata stays fresh. Set 0 to refresh every time.", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "selectionDepth": { + "default": 2, + "description": "Maximum depth for auto-generated GraphQL selection sets.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 5 + }, + "disabled": { + "default": false, + "description": "When true, omit this GraphQL Caplet.", + "type": "boolean" + } + }, + "required": ["name", "description", "endpointUrl", "auth"], + "additionalProperties": false + } + }, + "httpApis": { + "default": {}, + "description": "HTTP APIs keyed by stable Caplet ID.", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable HTTP API display name." + }, + "description": { + "type": "string", + "description": "Capability description shown to agents before HTTP actions are disclosed." + }, + "baseUrl": { + "type": "string", + "pattern": "^(?![a-zA-Z][a-zA-Z0-9+.-]*:\\/\\/[^/?#]*@)[^?#]*$", + "description": "Base URL for HTTP action requests.", + "format": "uri" + }, + "auth": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "none" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bearer" + }, + "token": { + "type": "string", + "minLength": 1 + } + }, + "required": ["type", "token"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "headers" + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "headers"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth2" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "issuer": { + "type": "string", + "format": "uri" + }, + "resourceMetadataUrl": { + "type": "string", + "format": "uri" + }, + "authorizationServerMetadataUrl": { + "type": "string", + "format": "uri" + }, + "openidConfigurationUrl": { + "type": "string", + "format": "uri" + }, + "clientMetadataUrl": { + "type": "string", + "format": "uri" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oidc" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "issuer": { + "type": "string", + "format": "uri" + }, + "resourceMetadataUrl": { + "type": "string", + "format": "uri" + }, + "authorizationServerMetadataUrl": { + "type": "string", + "format": "uri" + }, + "openidConfigurationUrl": { + "type": "string", + "format": "uri" + }, + "clientMetadataUrl": { + "type": "string", + "format": "uri" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "clientSecret": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "redirectUri": { + "type": "string", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false + } + ], + "description": "Explicit HTTP API request auth config. Use {\"type\":\"none\"} for public APIs." + }, + "actions": { + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"], + "description": "HTTP method used for this action." + }, + "path": { + "type": "string", + "minLength": 1, + "pattern": "^\\/", + "description": "URL path appended to the HTTP API baseUrl." + }, + "description": { + "description": "Action capability description.", + "type": "string", + "minLength": 1 + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "inputSchema": { + "description": "JSON Schema for call_tool arguments.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "outputSchema": { + "description": "JSON Schema for structuredContent returned by this action.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "query": { + "description": "Query parameter mapping.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "headers": { + "description": "Request header mapping.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "jsonBody": { + "description": "JSON request body mapping." + } + }, + "required": ["method", "path"], + "additionalProperties": false + }, + "description": "Configured HTTP actions keyed by stable tool name.", + "minProperties": 1 + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "exposure": { + "type": "string", + "enum": [ + "direct", + "progressive", + "code_mode", + "direct_and_code_mode", + "progressive_and_code_mode" + ], + "description": "How this Caplet is exposed to agents." + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "setup": { + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + }, + "verify": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true, + "description": "Requires Project Binding before this Caplet can run." + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + }, + "description": "Runtime features required by this Caplet." + }, + "resources": { + "description": "Hosted sandbox resource requirements.", + "type": "object", + "properties": { + "class": { + "description": "Requested hosted sandbox resource class.", + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + }, + "requestTimeoutMs": { + "default": 60000, + "description": "Timeout in milliseconds for HTTP action requests.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxResponseBytes": { + "default": 200000, + "description": "Maximum HTTP action response body bytes to read.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "default": false, + "description": "When true, omit this HTTP API Caplet.", + "type": "boolean" + } + }, + "required": ["name", "description", "baseUrl", "auth", "actions"], + "additionalProperties": false + } + }, + "cliTools": { + "default": {}, + "description": "CLI tools keyed by stable Caplet ID.", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable CLI tools display name." + }, + "description": { + "type": "string", + "description": "Capability description shown to agents before CLI actions are disclosed." + }, + "actions": { + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "description": { + "description": "Action capability description.", + "type": "string", + "minLength": 1 + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "inputSchema": { + "description": "JSON Schema for call_tool arguments.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "outputSchema": { + "description": "JSON Schema for structuredContent returned by this action.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables for the command.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this action.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "description": "Command timeout in milliseconds.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "description": "Maximum combined stdout and stderr bytes to keep.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "output": { + "type": "object", + "properties": { + "type": { + "default": "text", + "description": "How stdout should be represented in structuredContent.", + "type": "string", + "enum": ["text", "json"] + } + }, + "additionalProperties": false + }, + "annotations": { + "type": "object", + "properties": { + "readOnlyHint": { + "type": "boolean" + }, + "destructiveHint": { + "type": "boolean" + }, + "idempotentHint": { + "type": "boolean" + }, + "openWorldHint": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": ["command"], + "additionalProperties": false + }, + "description": "Configured CLI actions keyed by stable tool name.", + "minProperties": 1 + }, + "cwd": { + "description": "Default working directory for CLI actions.", + "type": "string", + "minLength": 1 + }, + "env": { + "description": "Default environment variables for CLI actions.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "exposure": { + "type": "string", + "enum": [ + "direct", + "progressive", + "code_mode", + "direct_and_code_mode", + "progressive_and_code_mode" + ], + "description": "How this Caplet is exposed to agents." + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "setup": { + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + }, + "verify": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true, + "description": "Requires Project Binding before this Caplet can run." + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + }, + "description": "Runtime features required by this Caplet." + }, + "resources": { + "description": "Hosted sandbox resource requirements.", + "type": "object", + "properties": { + "class": { + "description": "Requested hosted sandbox resource class.", + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + }, + "timeoutMs": { + "default": 60000, + "description": "Default timeout in milliseconds for CLI actions.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "default": 200000, + "description": "Default maximum combined stdout and stderr bytes to keep.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "disabled": { + "default": false, + "description": "When true, omit this CLI tools Caplet.", + "type": "boolean" + } + }, + "required": ["name", "description", "actions"], + "additionalProperties": false + } + }, + "capletSets": { + "default": {}, + "description": "Nested Caplet collections keyed by stable Caplet ID.", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]{1,64}$" + }, + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Human-readable Caplet set display name." + }, + "description": { + "type": "string", + "description": "Capability description shown before child Caplets are disclosed." + }, + "configPath": { + "description": "Child Caplets config.json path.", + "type": "string", + "minLength": 1 + }, + "capletsRoot": { + "description": "Child Markdown Caplets root directory.", + "type": "string", + "minLength": 1 + }, + "defaultSearchLimit": { + "default": 20, + "description": "Default maximum number of child Caplet search results.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxSearchLimit": { + "default": 50, + "description": "Maximum accepted child Caplet search result limit.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 50 + }, + "toolCacheTtlMs": { + "default": 30000, + "description": "Milliseconds child Caplet metadata stays fresh. Set 0 to refresh every time.", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "exposure": { + "type": "string", + "enum": [ + "direct", + "progressive", + "code_mode", + "direct_and_code_mode", + "progressive_and_code_mode" + ], + "description": "How this Caplet is exposed to agents." + }, + "useWhen": { + "description": "When agents should prefer this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "avoidWhen": { + "description": "When agents should avoid this Caplet or configured action.", + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "setup": { + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + }, + "verify": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "description": "Human-readable setup or verification step label." + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable command to spawn without a shell." + }, + "args": { + "description": "Arguments passed to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "description": "Additional environment variables.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "description": "Working directory for this command.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxOutputBytes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["label", "command"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "projectBinding": { + "type": "object", + "properties": { + "required": { + "type": "boolean", + "const": true, + "description": "Requires Project Binding before this Caplet can run." + } + }, + "required": ["required"], + "additionalProperties": false, + "description": "Project Binding requirements for Caplets that need an attached project." + }, + "runtime": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "type": "string", + "enum": ["docker", "browser"] + }, + "description": "Runtime features required by this Caplet." + }, + "resources": { + "description": "Hosted sandbox resource requirements.", + "type": "object", + "properties": { + "class": { + "description": "Requested hosted sandbox resource class.", + "type": "string", + "enum": ["standard", "large", "heavy"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "description": "Runtime feature and resource requirements for hosted execution." + }, + "disabled": { + "default": false, + "description": "When true, omit this Caplet set.", + "type": "boolean" + } + }, + "required": ["name", "description"], + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/apps/landing/src/pages/index.astro b/apps/landing/src/pages/index.astro index 65fd1b36..ce2564ff 100644 --- a/apps/landing/src/pages/index.astro +++ b/apps/landing/src/pages/index.astro @@ -123,9 +123,9 @@ opencode`, }; const remoteEndpoints = [ - { label: "MCP", value: "/caplets/mcp" }, - { label: "Control", value: "/caplets/control" }, - { label: "Health", value: "/caplets/healthz" }, + { label: "MCP", value: "/caplets/v1/mcp" }, + { label: "Admin", value: "/caplets/v1/admin" }, + { label: "Health", value: "/caplets/v1/healthz" }, ]; const remoteComparison = [ diff --git a/apps/landing/test/campaign-copy.check.mjs b/apps/landing/test/campaign-copy.check.mjs index fa1180dc..56398281 100644 --- a/apps/landing/test/campaign-copy.check.mjs +++ b/apps/landing/test/campaign-copy.check.mjs @@ -28,7 +28,7 @@ const required = [ "caplets.github", "operation: search_tools", "GitHub / Context7 / DeepWiki / Playwright / Git / LSP", - "Live Pi eval", + "live Pi eval", "Real-world large MCP stack", "GitHub, Context7, DeepWiki, Git, filesystem, Playwright, ast-grep, language-server, and web-search", "Total Tokens", @@ -54,7 +54,7 @@ const required = [ "Caplets progressive", "Direct vanilla MCP", "Executor.sh", - "Choose direct tools, progressive discovery, Code Mode, or mixed exposure", + "Exposure modes include direct, progressive, code_mode, direct_and_code_mode, and progressive_and_code_mode.", "Remote Caplets server", "Auth into tools once. Use them from every agent.", "Provider tokens and OAuth state stay with that", @@ -68,16 +68,16 @@ const required = [ "caplets serve --transport http", "CAPLETS_MODE=remote", "opencode", - "/caplets/mcp", - "/caplets/control", - "/caplets/healthz", + "/caplets/v1/mcp", + "/caplets/v1/admin", + "/caplets/v1/healthz", "remote server command", "remote client command", "Codex", "OpenCode", "Pi", "caplets setup", - "Run setup, then add a Caplet.", + "Run setup, then add one Caplet.", "lets you choose which agent integrations to wire up", "caplets install spiritledsoftware/caplets github", "caplets install spiritledsoftware/caplets sourcegraph", diff --git a/apps/landing/test/schema-assets.test.ts b/apps/landing/test/schema-assets.test.ts new file mode 100644 index 00000000..1a7c6a34 --- /dev/null +++ b/apps/landing/test/schema-assets.test.ts @@ -0,0 +1,17 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const repoRoot = join(import.meta.dirname, "../../.."); + +describe("schema assets", () => { + it.each([ + ["config.schema.json", "schemas/caplets-config.schema.json"], + ["caplet-frontmatter.schema.json", "schemas/caplet.schema.json"], + ])("publishes %s from the generated schema source", (assetName, schemaPath) => { + const publicAsset = readFileSync(join(repoRoot, "apps/landing/public", assetName), "utf8"); + const generatedSchema = readFileSync(join(repoRoot, schemaPath), "utf8"); + + expect(publicAsset).toBe(generatedSchema); + }); +}); diff --git a/caplets/ast-grep/CAPLET.md b/caplets/ast-grep/CAPLET.md index 8110f231..34b26b66 100644 --- a/caplets/ast-grep/CAPLET.md +++ b/caplets/ast-grep/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: ast-grep description: Search, scan, test, rewrite, and scaffold ast-grep rules through curated MCP tools. tags: diff --git a/caplets/coding-agent-toolkit/CAPLET.md b/caplets/coding-agent-toolkit/CAPLET.md index 2550a5ca..d6656e92 100644 --- a/caplets/coding-agent-toolkit/CAPLET.md +++ b/caplets/coding-agent-toolkit/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: Coding Agent Toolkit description: self-contained nested toolkit of high-value Caplets for coding agents. tags: diff --git a/caplets/context7/CAPLET.md b/caplets/context7/CAPLET.md index 341b19dc..3f33abff 100644 --- a/caplets/context7/CAPLET.md +++ b/caplets/context7/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: Context7 Documentation description: Fetch current library and framework documentation through Context7 before using version-sensitive APIs. tags: diff --git a/caplets/deepwiki/CAPLET.md b/caplets/deepwiki/CAPLET.md index 83f3ce58..9d72db66 100644 --- a/caplets/deepwiki/CAPLET.md +++ b/caplets/deepwiki/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: DeepWiki description: Query repository-focused documentation and codebase explanations through DeepWiki's MCP service. tags: diff --git a/caplets/github/CAPLET.md b/caplets/github/CAPLET.md index 7d65e514..542e7973 100644 --- a/caplets/github/CAPLET.md +++ b/caplets/github/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: GitHub description: Inspect and manage GitHub repositories, issues, pull requests, branches, commits, and code review workflows. tags: diff --git a/caplets/linear/CAPLET.md b/caplets/linear/CAPLET.md index b0ab2151..34c08cfe 100644 --- a/caplets/linear/CAPLET.md +++ b/caplets/linear/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: Linear description: Plan and track product work in Linear by reading teams, projects, cycles, issues, comments, and workflow state. tags: diff --git a/caplets/lsp/CAPLET.md b/caplets/lsp/CAPLET.md index 270eb0df..796e6d6d 100644 --- a/caplets/lsp/CAPLET.md +++ b/caplets/lsp/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: LSP description: Language Server Protocol tools for project-aware code intelligence through language-server-mcp. tags: diff --git a/caplets/npm/CAPLET.md b/caplets/npm/CAPLET.md index e3411ba1..6eee131c 100644 --- a/caplets/npm/CAPLET.md +++ b/caplets/npm/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: npm Registry description: Query package metadata, versions, dist-tags, and search results from the public npm registry. tags: diff --git a/caplets/osv/CAPLET.md b/caplets/osv/CAPLET.md index 19fb17d2..2cbccfac 100644 --- a/caplets/osv/CAPLET.md +++ b/caplets/osv/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: OSV Vulnerabilities description: Query OSV.dev vulnerability data through explicit HTTP actions. tags: diff --git a/caplets/playwright/CAPLET.md b/caplets/playwright/CAPLET.md index d692073c..477e6001 100644 --- a/caplets/playwright/CAPLET.md +++ b/caplets/playwright/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: Playwright description: Drive a browser through Playwright MCP for frontend testing, inspection, and automation workflows. tags: diff --git a/caplets/pypi/CAPLET.md b/caplets/pypi/CAPLET.md index 54810522..dacc7416 100644 --- a/caplets/pypi/CAPLET.md +++ b/caplets/pypi/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: PyPI description: Query Python package metadata, releases, files, vulnerabilities, and Simple API project details from PyPI. tags: diff --git a/caplets/repo-cli/CAPLET.md b/caplets/repo-cli/CAPLET.md index e7fd2c84..c0332ea9 100644 --- a/caplets/repo-cli/CAPLET.md +++ b/caplets/repo-cli/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: Repository CLI description: Inspect and run common local repository workflows through curated CLI tools. tags: diff --git a/caplets/sourcegraph/CAPLET.md b/caplets/sourcegraph/CAPLET.md index 65276ff2..ef62c3a2 100644 --- a/caplets/sourcegraph/CAPLET.md +++ b/caplets/sourcegraph/CAPLET.md @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json +# yaml-language-server: $schema=https://caplets.dev/caplet-frontmatter.schema.json name: Sourcegraph description: Search and inspect code across Sourcegraph using its MCP endpoint for repository-aware coding workflows. tags: diff --git a/config.example.json b/config.example.json index c9716160..9f9aaea3 100644 --- a/config.example.json +++ b/config.example.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplets-config.schema.json", + "$schema": "https://caplets.dev/config.schema.json", "defaultSearchLimit": 20, "maxSearchLimit": 50, "mcpServers": { diff --git a/docs/architecture.md b/docs/architecture.md index e3bd3f0b..5a19ec17 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -45,7 +45,11 @@ The global default is `code_mode`. Per-Caplet config may choose `direct`, `progr - Progressive exposure registers one wrapper tool per Caplet. - Direct exposure registers discovered downstream MCP tools, resources, resource templates, and prompts. -The HTTP server in `packages/core/src/serve/http.ts` exposes MCP, control, and health endpoints for self-hosting and remote clients. Stdio remains the local MCP transport for ordinary client config. +The HTTP server in `packages/core/src/serve/http.ts` exposes versioned MCP, attach, admin, and health endpoints for self-hosting and remote clients. Stdio remains the local MCP transport for ordinary client config. + +`/v1/mcp` is the configured agent-facing MCP surface. It honors exposure policy, so a default `code_mode` server can expose only the `code_mode` tool to ordinary MCP clients. + +`/v1/attach` is the Caplets runtime attach API. Attached clients read `/v1/attach/manifest`, subscribe to `/v1/attach/events`, and invoke revision-scoped exports through `/v1/attach/invoke` before merging remote projections with local/project overlays. ### Code Mode diff --git a/docs/native-integrations.md b/docs/native-integrations.md index 0ce55c55..bf3de95c 100644 --- a/docs/native-integrations.md +++ b/docs/native-integrations.md @@ -17,6 +17,8 @@ OpenCode and Pi use the same resolver as `caplets attach`. Cloud mode starts Project Binding automatically for the current project and overlays local/project Caplets over the remote workspace. +`caplets attach` and native remote integrations connect to the remote `/v1/attach` API for the Caplets runtime surface. Ordinary MCP clients continue to use `/v1/mcp`, which remains governed by configured exposure policy. + Native metadata should expose: - auth mode: `hosted_cloud`, `self_hosted_remote`, or `unconfigured` diff --git a/docs/project-binding.md b/docs/project-binding.md index 29883e3e..9fed7fcc 100644 --- a/docs/project-binding.md +++ b/docs/project-binding.md @@ -10,6 +10,8 @@ Project Binding connects one local project root to a remote Caplets runtime so p Hosted Cloud uses `caplets cloud auth login` and a Selected Workspace. Self-hosted remotes use `CAPLETS_REMOTE_URL` plus `CAPLETS_REMOTE_TOKEN` or Basic Auth credentials. Passing `--workspace` must match the saved Selected Workspace; use `caplets cloud auth switch ` for an explicit switch. +The attach client connects to the remote `/v1/attach` API for runtime Caplet discovery and calls. `/v1/mcp` remains the ordinary agent-facing MCP endpoint and continues to honor configured exposure policy. + ## Binding Session Loop Foreground attach creates a server-side Binding Session through the Project Binding control API, opens the control WebSocket, emits normalized JSON events, sends periodic heartbeats, and sends a remote session-end request when interrupted. A single reconnectable WebSocket close emits a `reconnecting` event before retrying the same Binding Session. diff --git a/package.json b/package.json index c01c4964..e35cc1b6 100644 --- a/package.json +++ b/package.json @@ -37,18 +37,18 @@ }, "devDependencies": { "@changesets/cli": "^2.31.0", - "@cloudflare/workers-types": "^4.20260610.1", - "@types/node": "^25.9.2", - "@typescript/native-preview": "7.0.0-dev.20260610.1", + "@cloudflare/workers-types": "^4.20260613.1", + "@types/node": "^25.9.3", + "@typescript/native-preview": "7.0.0-dev.20260613.1", "alchemy": "0.93.12", "husky": "^9.1.7", "lint-staged": "^17.0.7", "oxfmt": "^0.54.0", "oxlint": "^1.69.0", "prettier-plugin-astro": "^0.14.1", - "rolldown": "^1.1.0", + "rolldown": "^1.1.1", "tsx": "^4.22.4", - "turbo": "^2.9.17", + "turbo": "^2.9.18", "typescript": "^6.0.3", "vitest": "^4.1.8" }, diff --git a/packages/benchmarks/package.json b/packages/benchmarks/package.json index 9922e514..92552ae4 100644 --- a/packages/benchmarks/package.json +++ b/packages/benchmarks/package.json @@ -16,11 +16,11 @@ "dependencies": { "@caplets/core": "workspace:*", "commander": "^15.0.0", - "js-tiktoken": "^1.0.20" + "js-tiktoken": "^1.0.21" }, "devDependencies": { - "@types/node": "^25.9.1", - "@typescript/native-preview": "7.0.0-dev.20260603.1", + "@types/node": "^25.9.3", + "@typescript/native-preview": "7.0.0-dev.20260613.1", "caplets": "workspace:*", "typescript": "^6.0.3", "vitest": "^4.1.8" diff --git a/packages/cli/package.json b/packages/cli/package.json index b2033981..861c424e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -45,13 +45,13 @@ "@modelcontextprotocol/sdk": "^1.29.0" }, "devDependencies": { - "@types/node": "^25.9.1", - "@typescript/native-preview": "7.0.0-dev.20260603.1", - "rolldown": "^1.0.3", + "@types/node": "^25.9.3", + "@typescript/native-preview": "7.0.0-dev.20260613.1", + "rolldown": "^1.1.1", "typescript": "^6.0.3", "vitest": "^4.1.8" }, "engines": { - "node": ">=22" + "node": ">=24" } } diff --git a/packages/core/package.json b/packages/core/package.json index 1f0d582e..7e49e4ef 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -93,13 +93,14 @@ }, "dependencies": { "@apidevtools/swagger-parser": "^12.1.0", + "@babel/parser": "^7.29.7", "@hono/mcp": "^0.3.0", "@hono/node-server": "^2.0.4", "@modelcontextprotocol/sdk": "^1.29.0", "ajv": "^8.20.0", "commander": "^15.0.0", - "graphql": "^16.14.1", - "hono": "^4.12.23", + "graphql": "^16.14.2", + "hono": "^4.12.25", "quickjs-emscripten": "^0.32.0", "typescript": "^6.0.3", "vfile": "^6.0.3", @@ -108,12 +109,12 @@ "zod": "^4.4.3" }, "devDependencies": { - "@types/node": "^25.9.1", - "@typescript/native-preview": "7.0.0-dev.20260603.1", - "rolldown": "^1.0.3", + "@types/node": "^25.9.3", + "@typescript/native-preview": "7.0.0-dev.20260613.1", + "rolldown": "^1.1.1", "vitest": "^4.1.8" }, "engines": { - "node": ">=22" + "node": ">=24" } } diff --git a/packages/core/src/attach/api.ts b/packages/core/src/attach/api.ts new file mode 100644 index 00000000..01f173c5 --- /dev/null +++ b/packages/core/src/attach/api.ts @@ -0,0 +1,517 @@ +import { createHash } from "node:crypto"; +import { schemaHash } from "../schema-hash"; +import { stableJsonStringify } from "../stable-json"; +import type { CapletsEngine } from "../engine"; +import { CapletsError, toSafeError } from "../errors"; +import { + decodeDirectResourceUri, + directResourceUriMatchesTemplate, +} from "../exposure/direct-names"; +import type { + CallableCaplet, + DirectPromptRegistration, + DirectResourceRegistration, + DirectResourceTemplateRegistration, + DirectToolRegistration, + ExposureSnapshot, +} from "../exposure/discovery"; +import { generatedToolInputJsonSchemaForCaplet } from "../generated-tool-input-schema"; + +export type AttachExportKind = + | "caplet" + | "tool" + | "resource" + | "resourceTemplate" + | "prompt" + | "completion"; + +export type AttachInvokeRequest = { + revision: string; + kind: AttachExportKind; + exportId: string; + input: unknown; +}; + +export type AttachDiagnostic = { + code: string; + message: string; + capletId?: string | undefined; + details?: unknown; +}; + +export type AttachManifestExport = { + stableId: string; + exportId: string; + kind: AttachExportKind; + name?: string | undefined; + uri?: string | undefined; + uriTemplate?: string | undefined; + title?: string | undefined; + description?: string | undefined; + inputSchema?: unknown; + outputSchema?: unknown; + annotations?: unknown; + schemaHash: string | null; + capletId: string; + shadowing: "forbid" | "allow"; +}; + +export type AttachProgressiveCapletExport = AttachManifestExport & { + kind: "caplet"; + name: string; +}; + +export type AttachToolExport = AttachManifestExport & { + kind: "tool"; + name: string; + downstreamName: string; +}; + +export type AttachResourceExport = AttachManifestExport & { + kind: "resource"; + uri: string; + downstreamUri: string; + mimeType?: string | undefined; + size?: number | undefined; +}; + +export type AttachResourceTemplateExport = AttachManifestExport & { + kind: "resourceTemplate"; + uriTemplate: string; + downstreamUriTemplate: string; + mimeType?: string | undefined; +}; + +export type AttachPromptExport = AttachManifestExport & { + kind: "prompt"; + name: string; + downstreamName: string; +}; + +export type AttachCompletionExport = AttachManifestExport & { + kind: "completion"; + name: string; +}; + +export type AttachCodeModeCaplet = AttachManifestExport & { + kind: "caplet"; + name: string; +}; + +export type AttachManifest = { + version: 1; + revision: string; + generatedAt: string; + caplets: AttachProgressiveCapletExport[]; + tools: AttachToolExport[]; + resources: AttachResourceExport[]; + resourceTemplates: AttachResourceTemplateExport[]; + prompts: AttachPromptExport[]; + completions: AttachCompletionExport[]; + codeModeCaplets: AttachCodeModeCaplet[]; + diagnostics: AttachDiagnostic[]; +}; + +type AttachRoute = + | { kind: "caplet"; capletId: string } + | { kind: "tool"; capletId: string; downstreamName: string } + | { kind: "resource"; capletId: string; downstreamUri: string } + | { kind: "resourceTemplate"; capletId: string; downstreamUriTemplate: string } + | { kind: "prompt"; capletId: string; downstreamName: string } + | { kind: "completion"; capletId: string }; + +export type AttachProjection = { + manifest: AttachManifest; + routes: Map; +}; + +type AttachManifestProjectionInput = { + caplets: Array>; + tools: Array>; + resources: Array>; + resourceTemplates: Array>; + prompts: Array>; + completions: Array>; + codeModeCaplets: Array>; + diagnostics: AttachDiagnostic[]; +}; + +export async function buildAttachProjection(engine: CapletsEngine): Promise { + const snapshot = await engine.exposureSnapshot(); + const partial = sortAttachProjectionInput({ + caplets: snapshot.progressiveCaplets.map(progressiveCapletExport), + tools: snapshot.directTools.map(toolExport), + resources: snapshot.directResources.map(resourceExport), + resourceTemplates: snapshot.directResourceTemplates.map(resourceTemplateExport), + prompts: snapshot.directPrompts.map(promptExport), + completions: completionExports(snapshot), + codeModeCaplets: snapshot.codeModeCaplets.map(codeModeCapletExport), + diagnostics: snapshot.hiddenCaplets.map((hidden) => ({ + code: `ATTACH_CAPLET_${hidden.reason.toUpperCase()}`, + message: `Caplet ${hidden.capletId} is not exported: ${hidden.reason}.`, + capletId: hidden.capletId, + ...(hidden.error ? { details: hidden.error } : {}), + })), + }); + const revision = revisionFor(partial); + const manifest: AttachManifest = { + version: 1, + revision, + generatedAt: new Date().toISOString(), + ...withRevisionExportIds(revision, partial), + }; + return { + manifest, + routes: routesFor(manifest), + }; +} + +export async function invokeAttachExport( + engine: CapletsEngine, + projection: AttachProjection, + request: AttachInvokeRequest, +): Promise { + if (request.revision !== projection.manifest.revision) { + throw new CapletsError("ATTACH_MANIFEST_STALE", "Attach manifest revision is stale."); + } + const route = projection.routes.get(request.exportId); + if (!route || route.kind !== request.kind) { + throw new CapletsError("ATTACH_EXPORT_NOT_FOUND", "Attach export was not found."); + } + if (route.kind === "caplet") { + return await engine.execute(route.capletId, request.input); + } + if (route.kind === "tool") { + return await engine.executeDirectTool( + route.capletId, + route.downstreamName, + isRecord(request.input) ? request.input : {}, + ); + } + if (route.kind === "resource") { + return await engine.readDirectResource(route.capletId, route.downstreamUri); + } + if (route.kind === "resourceTemplate") { + const uri = + isRecord(request.input) && typeof request.input.uri === "string" + ? request.input.uri + : undefined; + if (!uri) { + throw new CapletsError( + "REQUEST_INVALID", + "Attach resource template invoke requires input.uri.", + ); + } + const downstreamUri = downstreamResourceUri(route.capletId, uri); + if (!directResourceUriMatchesTemplate(downstreamUri, route.downstreamUriTemplate)) { + throw new CapletsError( + "ATTACH_EXPORT_NOT_FOUND", + "Attach resource URI does not match the exported resource template.", + ); + } + return await engine.readDirectResource(route.capletId, downstreamUri); + } + if (route.kind === "prompt") { + return await engine.getDirectPrompt( + route.capletId, + route.downstreamName, + isRecord(request.input) ? stringifyRecord(request.input) : {}, + ); + } + if (route.kind === "completion") { + return await engine.execute(route.capletId, { + ...normalizeCompletionInput(projection.manifest, route.capletId, request.input), + operation: "complete", + }); + } + throw new CapletsError( + "REQUEST_INVALID", + "Attach export kind is not invokable via /v1/attach/invoke.", + ); +} + +export function attachErrorResponse(error: unknown): { + status: 400 | 404 | 409 | 500; + body: { ok: false; error: { code: string; message: string; details?: unknown } }; +} { + const safe = toSafeError(error, "INTERNAL_ERROR"); + const status = + safe.code === "ATTACH_MANIFEST_STALE" + ? 409 + : safe.code === "ATTACH_EXPORT_NOT_FOUND" + ? 404 + : safe.code === "REQUEST_INVALID" + ? 400 + : 500; + return { status, body: { ok: false, error: safe } }; +} + +function progressiveCapletExport( + entry: CallableCaplet, +): Omit { + const inputSchema = generatedToolInputJsonSchemaForCaplet(entry.caplet); + return { + stableId: `progressive:${entry.caplet.server}`, + kind: "caplet", + name: entry.caplet.server, + title: entry.caplet.name, + description: entry.caplet.description, + inputSchema, + schemaHash: schemaHash(inputSchema), + capletId: entry.caplet.server, + shadowing: "forbid", + }; +} + +function codeModeCapletExport(entry: CallableCaplet): Omit { + return { + stableId: `code_mode:${entry.caplet.server}`, + kind: "caplet", + name: entry.caplet.name, + title: entry.caplet.name, + description: entry.caplet.description, + schemaHash: null, + capletId: entry.caplet.server, + shadowing: "forbid", + }; +} + +function toolExport(entry: DirectToolRegistration): Omit { + return { + stableId: `tool:${entry.caplet.server}:${entry.downstreamName}`, + kind: "tool", + name: entry.name, + downstreamName: entry.downstreamName, + title: entry.tool.name, + description: entry.tool.description, + inputSchema: entry.tool.inputSchema, + outputSchema: entry.tool.outputSchema, + annotations: entry.tool.annotations, + schemaHash: schemaHash({ input: entry.tool.inputSchema, output: entry.tool.outputSchema }), + capletId: entry.caplet.server, + shadowing: "forbid", + }; +} + +function resourceExport(entry: DirectResourceRegistration): Omit { + return { + stableId: `resource:${entry.caplet.server}:${entry.downstreamUri}`, + kind: "resource", + uri: entry.uri, + downstreamUri: entry.downstreamUri, + title: entry.resource.name, + description: entry.resource.description, + ...(entry.resource.mimeType ? { mimeType: entry.resource.mimeType } : {}), + ...(typeof entry.resource.size === "number" ? { size: entry.resource.size } : {}), + schemaHash: null, + capletId: entry.caplet.server, + shadowing: "forbid", + }; +} + +function resourceTemplateExport( + entry: DirectResourceTemplateRegistration, +): Omit { + return { + stableId: `resourceTemplate:${entry.caplet.server}:${entry.downstreamUriTemplate}`, + kind: "resourceTemplate", + uriTemplate: entry.uriTemplate, + downstreamUriTemplate: entry.downstreamUriTemplate, + title: entry.resourceTemplate.name, + description: entry.resourceTemplate.description, + ...(entry.resourceTemplate.mimeType ? { mimeType: entry.resourceTemplate.mimeType } : {}), + schemaHash: null, + capletId: entry.caplet.server, + shadowing: "forbid", + }; +} + +function promptExport(entry: DirectPromptRegistration): Omit { + const inputSchema = { arguments: entry.prompt.arguments ?? [] }; + return { + stableId: `prompt:${entry.caplet.server}:${entry.downstreamName}`, + kind: "prompt", + name: entry.name, + downstreamName: entry.downstreamName, + title: entry.prompt.name, + description: entry.prompt.description, + inputSchema, + schemaHash: schemaHash(inputSchema), + capletId: entry.caplet.server, + shadowing: "forbid", + }; +} + +function completionExports( + snapshot: ExposureSnapshot, +): Array> { + const capletIds = new Set([ + ...snapshot.directPrompts.map((entry) => entry.caplet.server), + ...snapshot.directResourceTemplates.map((entry) => entry.caplet.server), + ]); + return [...capletIds].sort().map((capletId) => ({ + stableId: `completion:${capletId}`, + kind: "completion", + name: `${capletId}:complete`, + title: "Complete", + description: `MCP completion for ${capletId}.`, + schemaHash: null, + capletId, + shadowing: "forbid", + })); +} + +function sortAttachProjectionInput( + partial: AttachManifestProjectionInput, +): AttachManifestProjectionInput { + return { + caplets: sortByStableId(partial.caplets), + tools: sortByStableId(partial.tools), + resources: sortByStableId(partial.resources), + resourceTemplates: sortByStableId(partial.resourceTemplates), + prompts: sortByStableId(partial.prompts), + completions: sortByStableId(partial.completions), + codeModeCaplets: sortByStableId(partial.codeModeCaplets), + diagnostics: [...partial.diagnostics].sort((left, right) => + diagnosticSortKey(left).localeCompare(diagnosticSortKey(right)), + ), + }; +} + +function sortByStableId(entries: T[]): T[] { + return [...entries].sort((left, right) => left.stableId.localeCompare(right.stableId)); +} + +function diagnosticSortKey(diagnostic: AttachDiagnostic): string { + return stableJsonStringify({ + code: diagnostic.code, + capletId: diagnostic.capletId ?? "", + message: diagnostic.message, + }); +} + +function revisionFor(value: unknown): string { + return `sha256:${createHash("sha256").update(stableJsonStringify(value)).digest("hex")}`; +} + +function withRevisionExportIds( + revision: string, + partial: AttachManifestProjectionInput, +): Omit { + return { + ...partial, + caplets: partial.caplets.map((entry) => withExportId(revision, entry)), + tools: partial.tools.map((entry) => withExportId(revision, entry)), + resources: partial.resources.map((entry) => withExportId(revision, entry)), + resourceTemplates: partial.resourceTemplates.map((entry) => withExportId(revision, entry)), + prompts: partial.prompts.map((entry) => withExportId(revision, entry)), + completions: partial.completions.map((entry) => withExportId(revision, entry)), + codeModeCaplets: partial.codeModeCaplets.map((entry) => withExportId(revision, entry)), + }; +} + +function withExportId( + revision: string, + entry: T, +): T & { exportId: string } { + return { ...entry, exportId: `${revision}:${entry.stableId}` }; +} + +function routesFor(manifest: AttachManifest): Map { + const routes = new Map(); + for (const entry of manifest.caplets) { + routes.set(entry.exportId, { kind: "caplet", capletId: entry.capletId }); + } + for (const entry of manifest.tools) { + routes.set(entry.exportId, { + kind: "tool", + capletId: entry.capletId, + downstreamName: entry.downstreamName, + }); + } + for (const entry of manifest.resources) { + routes.set(entry.exportId, { + kind: "resource", + capletId: entry.capletId, + downstreamUri: entry.downstreamUri, + }); + } + for (const entry of manifest.resourceTemplates) { + routes.set(entry.exportId, { + kind: "resourceTemplate", + capletId: entry.capletId, + downstreamUriTemplate: entry.downstreamUriTemplate, + }); + } + for (const entry of manifest.prompts) { + routes.set(entry.exportId, { + kind: "prompt", + capletId: entry.capletId, + downstreamName: entry.downstreamName, + }); + } + for (const entry of manifest.completions) { + routes.set(entry.exportId, { kind: "completion", capletId: entry.capletId }); + } + for (const entry of manifest.codeModeCaplets) { + routes.set(entry.exportId, { kind: "caplet", capletId: entry.capletId }); + } + return routes; +} + +function normalizeCompletionInput( + manifest: AttachManifest, + capletId: string, + input: unknown, +): Record { + if (!isRecord(input)) return {}; + const ref = input.ref; + if (!isRecord(ref)) return input; + + if (ref.type === "prompt" && typeof ref.name === "string") { + const prompt = manifest.prompts.find( + (entry) => + entry.capletId === capletId && + (entry.name === ref.name || entry.downstreamName === ref.name), + ); + if (!prompt) return input; + return { ...input, ref: { ...ref, name: prompt.downstreamName } }; + } + + if (ref.type === "resourceTemplate" && typeof ref.uri === "string") { + const resourceTemplate = manifest.resourceTemplates.find( + (entry) => + entry.capletId === capletId && + (entry.uriTemplate === ref.uri || entry.downstreamUriTemplate === ref.uri), + ); + if (!resourceTemplate) return input; + return { ...input, ref: { ...ref, uri: resourceTemplate.downstreamUriTemplate } }; + } + + return input; +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function stringifyRecord(record: Record): Record { + return Object.fromEntries( + Object.entries(record).map(([key, value]) => [ + key, + typeof value === "string" ? value : JSON.stringify(value), + ]), + ); +} + +function downstreamResourceUri(capletId: string, uri: string): string { + if (!uri.startsWith("caplets://")) return uri; + const decoded = decodeDirectResourceUri(uri); + if (decoded.capletId !== capletId) { + throw new CapletsError( + "ATTACH_EXPORT_NOT_FOUND", + "Attach resource template URI belongs to a different Caplet.", + ); + } + return decoded.downstreamUri; +} diff --git a/packages/core/src/attach/options.ts b/packages/core/src/attach/options.ts index 22a2108a..a606e965 100644 --- a/packages/core/src/attach/options.ts +++ b/packages/core/src/attach/options.ts @@ -20,10 +20,24 @@ export async function resolveAttachServeOptions( env: Record = process.env, ): Promise { const selection = await resolveRemoteSelection(raw, env); - const serve = resolveServeOptions(raw, env); + const serve = resolveServeOptions(attachLocalServeOptions(raw), env); return { ...serve, projectRoot: raw.projectRoot ?? process.cwd(), selection, }; } + +function attachLocalServeOptions(raw: RawAttachServeOptions): RawServeOptions { + const { + user: _user, + password: _password, + token: _token, + remoteUrl: _remoteUrl, + workspace: _workspace, + fetch: _fetch, + projectRoot: _projectRoot, + ...serve + } = raw; + return serve; +} diff --git a/packages/core/src/attach/server.ts b/packages/core/src/attach/server.ts index c1faecac..c6043f92 100644 --- a/packages/core/src/attach/server.ts +++ b/packages/core/src/attach/server.ts @@ -59,7 +59,7 @@ function createAttachNativeService(options: AttachServeOptions, io: AttachServeI ...resolved, requestInit: options.selection.remote.requestInit, auth: nativeAuthFromRemoteAuth(options.selection.remote.auth), - url: options.selection.remote.mcpUrl, + url: options.selection.remote.attachUrl, ...(options.selection.remote.fetch ? { fetch: options.selection.remote.fetch } : {}), }), ...(io.writeErr ? { writeErr: io.writeErr } : {}), diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index 46d4ca22..b66388f4 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -146,6 +146,56 @@ export async function genericOAuthHeaders( return { authorization: `${bundle.tokenType ?? "Bearer"} ${bundle.accessToken}` }; } +export async function refreshOAuthTokenBundle( + target: CapletServerConfig | GenericAuthTarget, + authDir?: string, +): Promise { + if (target.auth?.type !== "oauth2" && target.auth?.type !== "oidc") { + throw new CapletsError("AUTH_REFRESH_FAILED", `${target.server} is not configured for OAuth`, { + server: target.server, + }); + } + const genericTarget = authRefreshTarget(target); + const authConfig = target.auth as OAuthLikeAuthConfig; + const bundle = readTokenBundle(target.server, authDir); + if (!bundle?.refreshToken) { + throw new CapletsError( + "AUTH_REFRESH_FAILED", + `OAuth refresh token required for ${target.server}`, + { + server: target.server, + backend: genericTarget.backend, + authType: authConfig.type, + nextAction: "run_caplets_auth_login", + }, + ); + } + const normalized = stripUndefined({ + ...bundle, + server: target.server, + authType: bundle.authType ?? authConfig.type, + clientId: bundle.clientId ?? authConfig.clientId ?? authConfig.clientMetadataUrl, + clientSecret: bundle.clientSecret ?? authConfig.clientSecret, + protectedResourceOrigin: + bundle.protectedResourceOrigin ?? protectedResourceOrigin(genericTarget, authConfig), + }) as StoredOAuthTokenBundle; + assertTokenBundleMatchesTarget(normalized, genericTarget, authConfig); + return refreshGenericOAuthBundle(genericTarget, authConfig, normalized, authDir); +} + +function authRefreshTarget(target: CapletServerConfig | GenericAuthTarget): GenericAuthTarget { + if (target.backend !== "mcp") { + return target; + } + return { + server: target.server, + backend: "http", + ...(target.url ? { url: target.url } : {}), + ...(target.auth ? { auth: target.auth } : {}), + requestTimeoutMs: target.callTimeoutMs, + }; +} + export class FileOAuthProvider implements OAuthClientProvider { private verifier = base64url(randomBytes(32)); private readonly stateValue = base64url(randomBytes(24)); @@ -157,6 +207,7 @@ export class FileOAuthProvider implements OAuthClientProvider { readonly redirectUrl: string, private readonly onRedirect: (url: URL) => void, private readonly authDir?: string, + private readonly options: { ignoreLegacyDynamicTokens?: boolean } = {}, ) { if ( (this.server.auth?.type === "oauth2" || this.server.auth?.type === "oidc") && @@ -202,6 +253,14 @@ export class FileOAuthProvider implements OAuthClientProvider { ...(this.server.auth.clientSecret ? { client_secret: this.server.auth.clientSecret } : {}), }; } + const bundle = readTokenBundle(this.server.server, this.authDir); + if (bundle?.clientId) { + return { + ...this.clientMetadata, + client_id: bundle.clientId, + ...(bundle.clientSecret ? { client_secret: bundle.clientSecret } : {}), + }; + } return undefined; } @@ -214,6 +273,9 @@ export class FileOAuthProvider implements OAuthClientProvider { if (!bundle) { return undefined; } + if (this.options.ignoreLegacyDynamicTokens && this.isLegacyDynamicTokenBundle(bundle)) { + return undefined; + } return stripUndefined({ access_token: bundle.accessToken, token_type: bundle.tokenType, @@ -225,7 +287,18 @@ export class FileOAuthProvider implements OAuthClientProvider { }) as OAuthTokens; } + private isLegacyDynamicTokenBundle(bundle: StoredOAuthTokenBundle): boolean { + return Boolean( + (this.server.auth?.type === "oauth2" || this.server.auth?.type === "oidc") && + !this.server.auth.clientId && + !this.server.auth.clientMetadataUrl && + !bundle.clientId && + (bundle.accessToken || bundle.refreshToken), + ); + } + saveTokens(tokens: OAuthTokens): void { + const clientInformation = this.clientInformation(); writeTokenBundle( stripUndefined({ server: this.server.server, @@ -236,6 +309,8 @@ export class FileOAuthProvider implements OAuthClientProvider { ? new Date(Date.now() + tokens.expires_in * 1000).toISOString() : undefined, scope: tokens.scope, + clientId: clientInformation?.client_id, + clientSecret: clientInformation?.client_secret, }), this.authDir, ); @@ -299,6 +374,7 @@ export async function startOAuthFlow( options.print?.(`Open this URL to authorize ${server.server}:\n${url.toString()}`); }, options.authDir, + { ignoreLegacyDynamicTokens: true }, ); const scope = scopesFor(server.auth); try { @@ -962,12 +1038,16 @@ async function refreshGenericOAuthBundle( assertAllowedAuthUrl(tokenEndpoint, "token endpoint", allowLoopbackHttp); const clientId = authConfig.clientId ?? authConfig.clientMetadataUrl ?? bundle.clientId; if (!clientId) { - throw new CapletsError("AUTH_REFRESH_FAILED", "OAuth clientId is required to refresh tokens", { - server: target.server, - backend: target.backend, - authType: authConfig.type, - nextAction: "run_caplets_auth_login", - }); + throw new CapletsError( + "AUTH_REFRESH_FAILED", + `OAuth client information is missing for ${target.server}. Re-run caplets auth login ${target.server}.`, + { + server: target.server, + backend: target.backend, + authType: authConfig.type, + nextAction: "run_caplets_auth_login", + }, + ); } const clientSecret = authConfig.clientSecret ?? bundle.clientSecret; const params = new URLSearchParams({ diff --git a/packages/core/src/caplet-files-bundle.ts b/packages/core/src/caplet-files-bundle.ts index dbf84892..fda66e82 100644 --- a/packages/core/src/caplet-files-bundle.ts +++ b/packages/core/src/caplet-files-bundle.ts @@ -684,7 +684,7 @@ type CapletFileFrontmatter = z.infer; export function capletJsonSchema(): unknown { return patchCapletJsonSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", - $id: "https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json", + $id: "https://caplets.dev/caplet-frontmatter.schema.json", title: "Caplet file frontmatter", description: "YAML frontmatter schema for a Markdown Caplet file.", ...z.toJSONSchema(capletFileSchema, { io: "input" }), diff --git a/packages/core/src/cli.ts b/packages/core/src/cli.ts index ed21ffc5..ea5f2c6b 100644 --- a/packages/core/src/cli.ts +++ b/packages/core/src/cli.ts @@ -17,6 +17,7 @@ import { listLocalAuthRows, localAuthConfigForTarget, localAuthTargets, + refreshAuth, type AuthSource, type AuthStatusRow, } from "./cli/auth"; @@ -1758,6 +1759,36 @@ export function createProgram(io: CliIO = {}): Command { }); }); + auth + .command("refresh") + .description("Refresh stored OAuth credentials for a server.") + .argument("", "configured OAuth Caplet ID") + .option("--project", "refresh credentials for the project Caplets config target") + .option("-g, --global", "refresh credentials for the user Caplets config target") + .option("--remote", "refresh credentials in the remote server auth store") + .action(async (serverId: string, options: AuthTargetOptions) => { + const target = await resolveAuthTarget(serverId, options, io); + if (target === "remote") { + const remote = requireRemoteClientForTarget(io); + await remote.request("auth_refresh", { server: serverId }); + writeOut(`Refreshed remote OAuth credentials for \`${serverId}\`.\n`); + return; + } + const configPath = currentConfigPath(); + const projectConfigPath = envProjectConfigPath(env); + await refreshAuth(serverId, { + writeOut, + ...(configPath ? { configPath } : {}), + config: localAuthConfigForTarget({ + serverId, + ...(configPath ? { configPath } : {}), + ...(projectConfigPath ? { projectConfigPath } : {}), + source: target, + }), + ...(io.authDir ? { authDir: io.authDir } : {}), + }); + }); + auth .command("list") .description("List servers with stored OAuth credentials.") diff --git a/packages/core/src/cli/add.ts b/packages/core/src/cli/add.ts index 56798090..2a8463c7 100644 --- a/packages/core/src/cli/add.ts +++ b/packages/core/src/cli/add.ts @@ -453,7 +453,7 @@ function renderBackendCaplet( const description = `${label} backend Caplet generated by caplets add.`; const lines = [ "---", - "$schema: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json", + "$schema: https://caplets.dev/caplet-frontmatter.schema.json", `name: ${yamlString(name)}`, `description: ${yamlString(description)}`, "tags:", diff --git a/packages/core/src/cli/auth.ts b/packages/core/src/cli/auth.ts index 2a2fa817..ce09f2cf 100644 --- a/packages/core/src/cli/auth.ts +++ b/packages/core/src/cli/auth.ts @@ -4,6 +4,7 @@ import { deleteTokenBundle, isTokenBundleExpired, readTokenBundle, + refreshOAuthTokenBundle, runGenericOAuthFlow, runOAuthFlow, type GenericAuthTarget, @@ -90,6 +91,29 @@ export function logoutAuthResult( return { server: serverId, deleted: deleteTokenBundle(serverId, options.authDir) }; } +export async function refreshAuth( + serverId: string, + options: { + authDir?: string; + configPath?: string; + config?: CapletsConfig; + writeOut: (value: string) => void; + }, +): Promise { + await refreshAuthResult(serverId, options); + options.writeOut(`Refreshed OAuth credentials for \`${serverId}\`.\n`); +} + +export async function refreshAuthResult( + serverId: string, + options: { authDir?: string; configPath?: string; config?: CapletsConfig }, +): Promise<{ server: string }> { + const target = findAuthTarget(serverId, options.config ?? loadConfig(options.configPath)); + assertLoginTarget(target, serverId); + await refreshOAuthTokenBundle(target, options.authDir); + return { server: serverId }; +} + export function listAuth(options: { authDir?: string; configPath?: string; diff --git a/packages/core/src/cli/author.ts b/packages/core/src/cli/author.ts index a888dfd1..aeea66eb 100644 --- a/packages/core/src/cli/author.ts +++ b/packages/core/src/cli/author.ts @@ -211,7 +211,7 @@ function renderCaplet(input: { }): string { const lines = [ "---", - "$schema: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json", + "$schema: https://caplets.dev/caplet-frontmatter.schema.json", `name: ${yamlString(input.name)}`, `description: ${yamlString(input.description)}`, "tags:", diff --git a/packages/core/src/cli/commands.ts b/packages/core/src/cli/commands.ts index 9659ade1..2e398ff5 100644 --- a/packages/core/src/cli/commands.ts +++ b/packages/core/src/cli/commands.ts @@ -64,7 +64,7 @@ export const topLevelCommandNames = [ export const cliSubcommands = { [cliCommands.add]: ["cli", "mcp", "openapi", "graphql", "http"], - [cliCommands.auth]: ["login", "logout", "list"], + [cliCommands.auth]: ["login", "logout", "list", "refresh"], [cliCommands.cloud]: ["auth"], [cliCommands.codeMode]: ["types"], [cliCommands.completion]: [...completionShells], diff --git a/packages/core/src/cli/doctor.ts b/packages/core/src/cli/doctor.ts index 65c89e8e..4b870758 100644 --- a/packages/core/src/cli/doctor.ts +++ b/packages/core/src/cli/doctor.ts @@ -3,9 +3,17 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { createNativeCapletsService } from "../native/service"; import { findProjectRoot, fingerprintProjectRoot } from "../cloud/project-root"; -import { CloudAuthStore, redactedCloudAuthStatus } from "../cloud-auth/store"; +import { + CloudAuthStore, + redactedCloudAuthStatus, + type CloudAuthCredentials, +} from "../cloud-auth/store"; import { projectBindingWorkspacePaths } from "../project-binding/workspaces"; -import { resolveCapletsRemote } from "../remote/options"; +import { + resolveCapletsRemote, + resolveHostedCloudRemote, + resolveRemoteMode, +} from "../remote/options"; import { resolveCapletsServer } from "../server/options"; import type { MutagenProjectSyncDoctorData } from "../project-binding/mutagen"; import { generateCodeModeDeclarations } from "../code-mode/declarations"; @@ -46,9 +54,9 @@ export async function doctorJsonReport(options: DoctorOptions = {}): Promise) { +function resolveRemoteSection( + env: NodeJS.ProcessEnv | Record, + credentials?: CloudAuthCredentials | null, +) { try { - const remote = resolveCapletsRemote({}, env); + const mode = resolveRemoteMode({}, env); + const remote = + mode.mode === "cloud" + ? resolveHostedCloudRemote(hostedCloudDoctorInput(credentials), env) + : resolveCapletsRemote({}, env); return { configured: true, baseUrl: remote.baseUrl.href, @@ -243,6 +258,15 @@ function resolveRemoteSection(env: NodeJS.ProcessEnv | Record isCallExpression(node) && isFetchCallee(node.callee)); } export function hasExecutableImport(code: string): boolean { - const source = ts.createSourceFile( - CODE_MODE_STATIC_ANALYSIS_FILE, - code, - ts.ScriptTarget.ES2022, - true, - ts.ScriptKind.TS, - ); - let found = false; + return hasMatchingAstNode(code, isExecutableImportNode); +} - const visit = (node: ts.Node): void => { - if (found) return; - if ( - ts.isImportDeclaration(node) || - ts.isImportEqualsDeclaration(node) || - (ts.isExportDeclaration(node) && node.moduleSpecifier !== undefined) - ) { - found = true; - return; - } - if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { - found = true; - return; - } - ts.forEachChild(node, visit); - }; +function hasMatchingAstNode(code: string, predicate: (node: AstNode) => boolean): boolean { + const ast = parseCode(code); + if (!ast) return false; - visit(source); + let found = false; + visitAst(ast, (node) => { + if (found) return; + found = predicate(node); + }); return found; } -function maskLiteralsAndComments(code: string): string { - let output = ""; - let index = 0; - while (index < code.length) { - const char = code[index]!; - const next = code[index + 1]; - if (char === '"' || char === "'" || char === "`") { - const masked = maskQuoted(code, index, char); - output += masked.text; - index = masked.nextIndex; - continue; - } - if (char === "/" && next === "/") { - const masked = maskLineComment(code, index); - output += masked.text; - index = masked.nextIndex; - continue; - } - if (char === "/" && next === "*") { - const masked = maskBlockComment(code, index); - output += masked.text; - index = masked.nextIndex; - continue; - } - output += char; - index += 1; +function parseCode(code: string): AstNode | undefined { + try { + return parse(code, PARSER_OPTIONS) as unknown as AstNode; + } catch { + return undefined; } - return output; } -function maskQuoted( - code: string, - start: number, - quote: '"' | "'" | "`", -): { text: string; nextIndex: number } { - let text = " "; - let index = start + 1; - while (index < code.length) { - const char = code[index]!; - text += char === "\n" ? "\n" : " "; - index += char === "\\" ? 2 : 1; - if (char === quote) break; +function isExecutableImportNode(node: AstNode): boolean { + if ( + node.type === "ImportDeclaration" || + node.type === "TSImportEqualsDeclaration" || + (isExportDeclaration(node) && node.source != null) + ) { + return true; } - return { text, nextIndex: index }; + return isCallExpression(node) && isNode(node.callee) && node.callee.type === "Import"; } -function maskLineComment(code: string, start: number): { text: string; nextIndex: number } { - let text = " "; - let index = start + 2; - while (index < code.length && code[index] !== "\n") { - text += " "; - index += 1; +function isCallExpression(node: AstNode): node is AstNode & { callee: unknown } { + return ( + (node.type === "CallExpression" || node.type === "OptionalCallExpression") && "callee" in node + ); +} + +function isFetchCallee(value: unknown): boolean { + if (!isNode(value)) return false; + if (value.type === "Identifier") return value.name === "fetch"; + if (value.type === "MemberExpression" || value.type === "OptionalMemberExpression") { + return isGlobalFetchMember(value); } - return { text, nextIndex: index }; + return false; +} + +function isGlobalFetchMember(node: AstNode): boolean { + if (!isIdentifierNamed(node.object, "globalThis", "window", "self")) return false; + if (node.computed === true) return isStringLiteralNamed(node.property, "fetch"); + return isIdentifierNamed(node.property, "fetch"); +} + +function isExportDeclaration(node: AstNode): boolean { + return ( + node.type === "ExportAllDeclaration" || + node.type === "ExportDefaultDeclaration" || + node.type === "ExportNamedDeclaration" + ); } -function maskBlockComment(code: string, start: number): { text: string; nextIndex: number } { - let text = " "; - let index = start + 2; - while (index < code.length) { - const char = code[index]!; - const next = code[index + 1]; - text += char === "\n" ? "\n" : " "; - index += 1; - if (char === "*" && next === "/") { - text += " "; - index += 1; - break; +function visitAst(value: unknown, visit: (node: AstNode) => void): void { + if (!isNode(value)) return; + visit(value); + + for (const [key, child] of Object.entries(value)) { + if (key === "loc" || key === "start" || key === "end" || key === "extra") continue; + if (Array.isArray(child)) { + for (const item of child) visitAst(item, visit); + continue; } + visitAst(child, visit); } - return { text, nextIndex: index }; +} + +function isNode(value: unknown): value is AstNode { + return typeof value === "object" && value !== null && typeof (value as AstNode).type === "string"; +} + +function isIdentifierNamed(value: unknown, ...names: string[]): boolean { + return isNode(value) && value.type === "Identifier" && names.includes(String(value.name)); +} + +function isStringLiteralNamed(value: unknown, name: string): boolean { + return isNode(value) && value.type === "StringLiteral" && value.value === name; } diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index e8d9c7c1..b7f214ac 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1359,7 +1359,7 @@ const normalizedConfigFileSchema = configSchemaFor( export function configJsonSchema(): unknown { return patchConfigJsonSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", - $id: "https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplets-config.schema.json", + $id: "https://caplets.dev/config.schema.json", title: "Caplets config", description: "Configuration file for the Caplets progressive MCP disclosure gateway.", ...z.toJSONSchema(configFileSchema, { io: "input" }), diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index 54e4b84d..f9ecb854 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -22,6 +22,8 @@ export const CAPLETS_ERROR_CODES = [ "DOWNSTREAM_RESOURCE_ERROR", "DOWNSTREAM_PROMPT_ERROR", "DOWNSTREAM_COMPLETION_ERROR", + "ATTACH_MANIFEST_STALE", + "ATTACH_EXPORT_NOT_FOUND", "UNSUPPORTED_TRANSPORT", "INTERNAL_ERROR", ] as const; diff --git a/packages/core/src/exposure/direct-names.ts b/packages/core/src/exposure/direct-names.ts index 7c6b52e7..869eb882 100644 --- a/packages/core/src/exposure/direct-names.ts +++ b/packages/core/src/exposure/direct-names.ts @@ -37,8 +37,76 @@ export function decodeDirectResourceUri(uri: string): { if (!parsed.pathname.startsWith(prefix)) { throw new CapletsError("REQUEST_INVALID", `Invalid Caplets resource URI ${uri}`); } + let downstreamUri: string; + try { + downstreamUri = decodeURIComponent(parsed.pathname.slice(prefix.length)); + } catch (error) { + throw new CapletsError("REQUEST_INVALID", `Invalid Caplets resource URI ${uri}`, error); + } return { capletId: parsed.hostname, - downstreamUri: decodeURIComponent(parsed.pathname.slice(prefix.length)), + downstreamUri, }; } + +export function directResourceUriMatchesTemplate(uri: string, uriTemplate: string): boolean { + return new RegExp(`^${uriTemplatePattern(uriTemplate)}$`, "u").test(uri); +} + +function uriTemplatePattern(uriTemplate: string): string { + let pattern = ""; + let offset = 0; + for (const match of uriTemplate.matchAll(/\{([^}]+)\}/gu)) { + pattern += escapePattern(uriTemplate.slice(offset, match.index)); + pattern += uriTemplateExpressionPattern(match[1] ?? ""); + offset = match.index + match[0].length; + } + return pattern + escapePattern(uriTemplate.slice(offset)); +} + +function uriTemplateExpressionPattern(expression: string): string { + const operator = expression.match(/^[+#./;?&]/u)?.[0] ?? ""; + const variables = variableNames(operator ? expression.slice(1) : expression); + if (operator === "?") return namedExpansionPattern("?", "&", variables); + if (operator === "&") return namedExpansionPattern("&", "&", variables); + if (operator === ";") return namedExpansionPattern(";", ";", variables); + if (operator === "/") return optionalSequencePattern("/", "[^?#/]*", variables); + if (operator === ".") return optionalSequencePattern(".", "[^/?#.]*", variables); + if (operator === "+") return "[^?#]*"; + if (operator === "#") return "(?:#[^?]*)?"; + return "[^/?#]*"; +} + +function namedExpansionPattern(prefix: string, separator: string, variables: string[]): string { + if (variables.length === 0) return ""; + const escapedSeparator = escapePattern(separator); + const alternatives = variables.map((name, index) => { + const head = `${escapePattern(name)}=[^&#]*`; + const tail = variables + .slice(index + 1) + .map((nextName) => `(?:${escapedSeparator}${escapePattern(nextName)}=[^&#]*)?`) + .join(""); + return `${head}${tail}`; + }); + return `(?:${escapePattern(prefix)}(?:${alternatives.join("|")}))?`; +} + +function optionalSequencePattern( + prefix: string, + valuePattern: string, + variables: string[], +): string { + if (variables.length === 0) return ""; + return `(?:${escapePattern(prefix)}${valuePattern}){0,${variables.length}}`; +} + +function variableNames(expression: string): string[] { + return expression + .split(",") + .map((value) => value.replace(/[:*].*$/u, "").trim()) + .filter(Boolean); +} + +function escapePattern(value: string): string { + return value.replace(/([.*+?^${}()|[\]\\])/gu, "\\$1"); +} diff --git a/packages/core/src/native/options.ts b/packages/core/src/native/options.ts index f96b2e6a..760b7eff 100644 --- a/packages/core/src/native/options.ts +++ b/packages/core/src/native/options.ts @@ -1,7 +1,8 @@ import { CapletsError } from "../errors"; -import { mcpUrlForBase, type CapletsServerEnv, type CapletsServerInput } from "../server/options"; +import type { CapletsServerEnv, CapletsServerInput } from "../server/options"; import { resolveCapletsRemote, + resolveHostedCloudRemote, resolveRemoteMode, type CapletsRemoteAuth, type CapletsRemoteEnv, @@ -76,12 +77,10 @@ export function resolveNativeCapletsServiceOptions( }; const server = mode.mode === "cloud" - ? resolveCapletsRemote( - { - url: input.server?.url ?? env.CAPLETS_REMOTE_URL ?? "", - ...(serverFetch ? { fetch: serverFetch } : {}), - }, - {}, + ? resolveNativeHostedCloudRemote( + input.server?.url ?? env.CAPLETS_REMOTE_URL ?? "", + optionalWorkspace(input, env).workspace, + serverFetch, ) : resolveCapletsRemote(serverInput, env); @@ -89,7 +88,7 @@ export function resolveNativeCapletsServiceOptions( return { mode: mode.mode, remote: { - url: mcpUrlForBase(server.baseUrl), + url: server.attachUrl, auth: nativeAuthFromRemoteAuth(server.auth), pollIntervalMs: parsePollInterval(input.remote?.pollIntervalMs), requestInit: @@ -102,6 +101,29 @@ export function resolveNativeCapletsServiceOptions( }; } +function resolveNativeHostedCloudRemote( + url: string, + workspace: string | undefined, + fetch: typeof globalThis.fetch | undefined, +): ReturnType { + return resolveHostedCloudRemote({ + url, + ...(workspace ? { workspace } : {}), + ...(fetch ? { fetch } : {}), + }); +} + +function optionalWorkspace( + input: NativeCapletsServiceResolutionInput, + env: NativeCapletsEnv, +): { workspace?: string } { + const workspace = + input.remote?.cloud?.workspaceId ?? + env.CAPLETS_REMOTE_WORKSPACE ?? + env.CAPLETS_CLOUD_WORKSPACE_ID; + return workspace ? { workspace } : {}; +} + function nativeAuthFromRemoteAuth(auth: CapletsRemoteAuth): NativeRemoteAuthOptions { if (auth.type === "basic") { return { enabled: true, user: auth.user, password: auth.password }; diff --git a/packages/core/src/native/remote.ts b/packages/core/src/native/remote.ts index 33386ff7..e4e144f4 100644 --- a/packages/core/src/native/remote.ts +++ b/packages/core/src/native/remote.ts @@ -1,22 +1,31 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp"; -import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types"; - import { CapletsError } from "../errors"; +import { + decodeDirectResourceUri, + directResourceUriMatchesTemplate, +} from "../exposure/direct-names"; import { generatedToolInputJsonSchemaForCaplet, operations } from "../generated-tool-input-schema"; +import type { AttachCodeModeCaplet, AttachManifest, AttachManifestExport } from "../attach/api"; +import { runCodeMode } from "../code-mode/runner"; +import { codeModeRunInputJsonSchema, codeModeRunInputSchema } from "../code-mode/tool"; import type { ResolvedNativeCapletsServiceOptions } from "./options"; import type { NativeCapletsService, NativeCapletsToolsChangedListener, NativeCapletTool, } from "./service"; -import { nativeCapletToolName, nativeCodeModeToolId } from "./tools"; +import { nativeCapletToolName, nativeCodeModeToolId, nativeCodeModeToolName } from "./tools"; export type RemoteCapletsTool = { name: string; + capletId?: string | undefined; + sourceCapletId?: string | undefined; title?: string | undefined; description?: string | undefined; inputSchema?: unknown; + outputSchema?: unknown; + annotations?: unknown; + codeModeRun?: boolean | undefined; + codeModeCaplets?: AttachCodeModeCaplet[] | undefined; }; export type RemoteCapletsClient = { @@ -41,57 +50,119 @@ export type RemoteNativeCapletsServiceOptions = { export function createSdkRemoteCapletsClient( options: RemoteCapletsClientOptions["remote"], ): RemoteCapletsClient { - const client = new Client({ name: "caplets-native", version: "1.0.0" }, { capabilities: {} }); - const transport = new StreamableHTTPClientTransport(options.url, { - requestInit: options.requestInit, - ...(options.fetch ? { fetch: options.fetch } : {}), - }); - // The SDK transport type is narrower than StreamableHTTPClientTransport at compile time, - // but this is the documented transport used by the streamable HTTP client. - const ready = client.connect(transport as never); - const readyObserved = ready.catch(() => undefined); + const fetchImpl = options.fetch ?? fetch; const listeners = new Set<() => void>(); + let manifest: AttachManifest | undefined; + let exportByName = new Map(); + let eventsAbort: AbortController | undefined; + let eventsReconnectTimer: ReturnType | undefined; - client.setNotificationHandler(ToolListChangedNotificationSchema, () => { - for (const listener of listeners) { - listener(); + const clearEventsReconnectTimer = () => { + if (eventsReconnectTimer) { + clearTimeout(eventsReconnectTimer); + eventsReconnectTimer = undefined; } - }); + }; + const startEvents = () => { + if (eventsAbort || listeners.size === 0) return; + eventsAbort = startAttachEvents( + options.url, + options.requestInit, + fetchImpl, + listeners, + (closedAbort, retry) => { + if (eventsAbort !== closedAbort) return; + eventsAbort = undefined; + if (!retry || closedAbort.signal.aborted || listeners.size === 0) return; + clearEventsReconnectTimer(); + eventsReconnectTimer = setTimeout(() => { + eventsReconnectTimer = undefined; + startEvents(); + }, 1_000); + }, + ); + }; return { async listTools() { - await ready; - const result = await client.listTools(); - return (result.tools ?? []).map((tool) => ({ - name: tool.name, - ...(tool.title ? { title: tool.title } : {}), - ...(tool.description ? { description: tool.description } : {}), - ...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}), - })); + manifest = await fetchAttachManifest(options.url, options.requestInit, fetchImpl); + exportByName = exportMapFor(manifest); + return toolsFromManifest(manifest); }, async callTool(name, args) { - await ready; - const toolArguments = - args && typeof args === "object" && !Array.isArray(args) - ? (args as Record) - : undefined; - return await client.callTool({ name, arguments: toolArguments }); + if (!manifest) { + manifest = await fetchAttachManifest(options.url, options.requestInit, fetchImpl); + exportByName = exportMapFor(manifest); + } + const invokeWithStaleRetry = async ( + entry: AttachManifestExport, + input: unknown, + ): Promise => { + try { + return await invokeAttachExport(options.url, options.requestInit, fetchImpl, { + revision: manifest!.revision, + kind: entry.kind, + exportId: entry.exportId, + input, + }); + } catch (error) { + if (!isAttachManifestStale(error)) throw error; + const nextManifest = await fetchAttachManifest( + options.url, + options.requestInit, + fetchImpl, + ); + const nextEntry = compatibleExport(nextManifest, entry); + manifest = nextManifest; + exportByName = exportMapFor(nextManifest); + if (!nextEntry) { + throw new CapletsError( + "ATTACH_EXPORT_NOT_FOUND", + "Attach export changed after manifest refresh; refetch the manifest before retrying.", + ); + } + return await invokeAttachExport(options.url, options.requestInit, fetchImpl, { + revision: nextManifest.revision, + kind: nextEntry.kind, + exportId: nextEntry.exportId, + input, + }); + } + }; + const directTool = manifest.tools.find((entry) => entry.name === name); + if (directTool) return await invokeWithStaleRetry(directTool, args ?? {}); + const primitive = await callPrimitiveExport(manifest, name, args, invokeWithStaleRetry); + if (primitive.handled) return primitive.result; + const entry = exportByName.get(name); + if (!entry) { + throw new CapletsError("ATTACH_EXPORT_NOT_FOUND", `Attach export ${name} was not found.`); + } + return await invokeWithStaleRetry(entry, args ?? {}); }, onToolsChanged(listener) { listeners.add(listener); - return () => listeners.delete(listener); + startEvents(); + return () => { + listeners.delete(listener); + if (listeners.size === 0) { + clearEventsReconnectTimer(); + eventsAbort?.abort(); + eventsAbort = undefined; + } + }; }, async close() { + clearEventsReconnectTimer(); + eventsAbort?.abort(); + eventsAbort = undefined; listeners.clear(); - await readyObserved; - await transport.terminateSession().catch(() => undefined); - await client.close(); }, }; } export class RemoteNativeCapletsService implements NativeCapletsService { private tools: NativeCapletTool[] = []; + private toolRoutes = new Map(); private readonly listeners = new Set(); private readonly clientFactory: () => RemoteCapletsClient; private client: RemoteCapletsClient; @@ -115,8 +186,12 @@ export class RemoteNativeCapletsService implements NativeCapletsService { } async execute(capletId: string, request: unknown): Promise { + if (capletId === nativeCodeModeToolId) { + return await executeCodeModeRunRemote(this, request); + } + const remoteToolId = this.toolRoutes.get(capletId) ?? capletId; try { - return await this.client.callTool(capletId, request); + return await this.client.callTool(remoteToolId, request); } catch (error) { if (isAuthFailure(error)) { throw remoteAuthError(this.options.authKind ?? "self_hosted_remote"); @@ -126,7 +201,7 @@ export class RemoteNativeCapletsService implements NativeCapletsService { throw error; } try { - return await this.client.callTool(capletId, request); + return await this.client.callTool(remoteToolId, request); } catch (retryError) { if (isAuthFailure(retryError)) { throw remoteAuthError(this.options.authKind ?? "self_hosted_remote"); @@ -180,9 +255,11 @@ export class RemoteNativeCapletsService implements NativeCapletsService { } private async reloadFromClient(): Promise { - const tools = (await this.client.listTools()).map(remoteToolToNativeTool); + const remoteTools = await this.client.listTools(); + const tools = remoteTools.map(remoteToolToNativeTool); const changed = JSON.stringify(tools) !== JSON.stringify(this.tools); this.tools = tools; + this.toolRoutes = new Map(remoteTools.map((tool) => [nativeToolRouteId(tool), tool.name])); if (changed) { this.emitToolsChanged(); } @@ -238,27 +315,539 @@ export class RemoteNativeCapletsService implements NativeCapletsService { } function remoteToolToNativeTool(tool: RemoteCapletsTool): NativeCapletTool { - const toolName = nativeCapletToolName(tool.name); + const capletId = nativeToolRouteId(tool); + const sourceCaplet = tool.sourceCapletId ?? tool.capletId; + const toolName = tool.codeModeRun ? nativeCodeModeToolName : nativeCapletToolName(capletId); const inputSchema = isPlainObject(tool.inputSchema) ? tool.inputSchema : generatedToolInputJsonSchemaForCaplet({ backend: "tool" }); + const operationNames = + tool.sourceCapletId === undefined && !tool.codeModeRun + ? operationNamesFromSchema(inputSchema) + : undefined; return { - caplet: tool.name, + caplet: capletId, + ...(sourceCaplet && sourceCaplet !== capletId ? { sourceCaplet } : {}), toolName, - title: tool.title ?? tool.name, + title: tool.title ?? capletId, description: [ tool.description ?? "Remote Caplets tool.", "", `Native tool name: ${toolName}`, - `Remote Caplet ID: ${tool.name}`, + `Remote Caplet ID: ${capletId}`, ].join("\n"), promptGuidance: [`Use ${toolName} through the remote Caplets service.`], - ...(tool.name === nativeCodeModeToolId ? { codeModeRun: true } : {}), + ...(tool.codeModeRun || capletId === nativeCodeModeToolId ? { codeModeRun: true } : {}), + ...(tool.codeModeCaplets + ? { + codeModeCaplets: tool.codeModeCaplets.map((caplet) => ({ + id: caplet.capletId, + name: caplet.name, + description: caplet.description ?? "", + })), + } + : {}), inputSchema, - operationNames: operationNamesFromSchema(inputSchema), + ...(isPlainObject(tool.outputSchema) ? { outputSchema: tool.outputSchema } : {}), + ...(isPlainObject(tool.annotations) ? { annotations: tool.annotations } : {}), + ...(operationNames ? { operationNames } : {}), + }; +} + +function nativeToolRouteId(tool: RemoteCapletsTool): string { + return tool.codeModeRun ? nativeCodeModeToolId : tool.name; +} + +async function fetchAttachManifest( + attachUrl: URL, + requestInit: RequestInit | undefined, + fetchImpl: typeof fetch, +): Promise { + const response = await fetchImpl(new URL("manifest", slashUrl(attachUrl)), { + ...requestInit, + method: "GET", + }); + if (!response.ok) { + throw new CapletsError( + "SERVER_UNAVAILABLE", + `Caplets attach manifest returned HTTP ${response.status}.`, + ); + } + return (await response.json()) as AttachManifest; +} + +async function invokeAttachExport( + attachUrl: URL, + requestInit: RequestInit | undefined, + fetchImpl: typeof fetch, + body: { revision: string; kind: string; exportId: string; input: unknown }, +): Promise { + const headers = new Headers(requestInit?.headers); + headers.set("content-type", "application/json"); + const response = await fetchImpl(new URL("invoke", slashUrl(attachUrl)), { + ...requestInit, + method: "POST", + headers, + body: JSON.stringify(body), + }); + let payload: unknown; + try { + payload = (await response.json()) as unknown; + } catch (error) { + if (!response.ok) { + throw attachPayloadError( + { error: { message: `Caplets attach invoke returned HTTP ${response.status}.` } }, + response.status, + ); + } + throw error; + } + if (!response.ok) { + throw attachPayloadError(payload, response.status); + } + if (isPlainObject(payload) && payload.ok === true && "data" in payload) { + return payload.data; + } + return payload; +} + +async function callPrimitiveExport( + manifest: AttachManifest, + name: string, + args: unknown, + invoke: (entry: AttachManifestExport, input: unknown) => Promise, +): Promise<{ handled: false } | { handled: true; result: unknown }> { + const primitive = primitiveRoute(name); + if (!primitive) return { handled: false }; + const input = isPlainObject(args) ? args : {}; + const { capletId, operation } = primitive; + if (operation === "list_resources") { + const resources = manifest.resources.filter((entry) => entry.capletId === capletId); + if (resources.length === 0) return { handled: false }; + return { + handled: true, + result: { + items: resources.map((entry) => ({ + uri: entry.uri, + name: entry.title, + description: entry.description, + ...(entry.mimeType ? { mimeType: entry.mimeType } : {}), + ...(typeof entry.size === "number" ? { size: entry.size } : {}), + })), + }, + }; + } + if (operation === "list_resource_templates") { + const resourceTemplates = manifest.resourceTemplates.filter( + (entry) => entry.capletId === capletId, + ); + if (resourceTemplates.length === 0) return { handled: false }; + return { + handled: true, + result: { + items: resourceTemplates.map((entry) => ({ + uriTemplate: entry.uriTemplate, + name: entry.title, + description: entry.description, + ...(entry.mimeType ? { mimeType: entry.mimeType } : {}), + })), + }, + }; + } + if (operation === "list_prompts") { + const prompts = manifest.prompts.filter((entry) => entry.capletId === capletId); + if (prompts.length === 0) return { handled: false }; + return { + handled: true, + result: { + items: prompts.map((entry) => ({ + name: entry.name, + title: entry.title, + description: entry.description, + ...promptArguments(entry.inputSchema), + })), + }, + }; + } + + const entry = primitiveExport(manifest, capletId, operation, input); + if (!entry) return { handled: false }; + return { + handled: true, + result: await invoke(entry, primitiveInvokeInput(capletId, operation, input)), }; } +function primitiveRoute(name: string): { capletId: string; operation: string } | undefined { + for (const operation of [ + "list_resource_templates", + "list_resources", + "read_resource", + "list_prompts", + "get_prompt", + "complete", + ]) { + const suffix = `__${operation}`; + if (name.endsWith(suffix)) return { capletId: name.slice(0, -suffix.length), operation }; + } + return undefined; +} + +function primitiveExport( + manifest: AttachManifest, + capletId: string, + operation: string, + input: Record, +): AttachManifestExport | undefined { + if (operation === "read_resource") { + const uri = typeof input.uri === "string" ? input.uri : ""; + const resource = manifest.resources.find( + (entry) => entry.capletId === capletId && (entry.uri === uri || entry.downstreamUri === uri), + ); + if (resource) return resource; + + const downstreamUri = downstreamResourceUri(capletId, uri); + return manifest.resourceTemplates.find( + (entry) => + entry.capletId === capletId && + directResourceUriMatchesTemplate(downstreamUri, entry.downstreamUriTemplate), + ); + } + if (operation === "get_prompt") { + const name = typeof input.name === "string" ? input.name : ""; + return manifest.prompts.find( + (entry) => + entry.capletId === capletId && (entry.name === name || entry.downstreamName === name), + ); + } + if (operation === "complete") { + return manifest.completions.find((entry) => entry.capletId === capletId); + } + return undefined; +} + +function primitiveInvokeInput( + capletId: string, + operation: string, + input: Record, +): unknown { + if (operation === "read_resource") { + const uri = typeof input.uri === "string" ? input.uri : ""; + return { ...input, uri: downstreamResourceUri(capletId, uri) }; + } + if (operation === "get_prompt") return input.args ?? {}; + return input; +} + +function toolsFromManifest(manifest: AttachManifest): RemoteCapletsTool[] { + const codeModeMarker = attachCodeModeMarker(manifest); + return [ + ...manifest.caplets.map((entry) => ({ + name: entry.capletId, + capletId: entry.capletId, + title: entry.title ?? entry.name, + description: entry.description, + inputSchema: entry.inputSchema, + ...codeModeMarker, + })), + ...manifest.tools.map((entry) => ({ + name: entry.name, + capletId: entry.capletId, + sourceCapletId: entry.capletId, + title: entry.title ?? entry.name, + description: entry.description, + inputSchema: entry.inputSchema, + outputSchema: entry.outputSchema, + annotations: entry.annotations, + ...codeModeMarker, + })), + ...primitiveToolsFromManifest(manifest, codeModeMarker), + ...(manifest.codeModeCaplets.length > 0 + ? [ + { + name: nativeCodeModeToolId, + capletId: nativeCodeModeToolId, + title: "Code Mode", + description: "Remote Caplets available to locally-run attached Code Mode.", + codeModeRun: true, + codeModeCaplets: manifest.codeModeCaplets, + inputSchema: codeModeRunInputJsonSchema(), + }, + ] + : []), + ]; +} + +function attachCodeModeMarker( + manifest: AttachManifest, +): Pick | Record { + return manifest.codeModeCaplets.length === 0 ? { codeModeCaplets: [] } : {}; +} + +function primitiveToolsFromManifest( + manifest: AttachManifest, + codeModeMarker: Pick | Record, +): RemoteCapletsTool[] { + const directToolNames = new Set(manifest.tools.map((entry) => entry.name)); + const byCaplet = new Map< + string, + { + resources: boolean; + resourceTemplates: boolean; + prompts: boolean; + completions: boolean; + } + >(); + const entryFor = (capletId: string) => { + const existing = byCaplet.get(capletId); + if (existing) return existing; + const next = { + resources: false, + resourceTemplates: false, + prompts: false, + completions: false, + }; + byCaplet.set(capletId, next); + return next; + }; + for (const entry of manifest.resources) entryFor(entry.capletId).resources = true; + for (const entry of manifest.resourceTemplates) entryFor(entry.capletId).resourceTemplates = true; + for (const entry of manifest.prompts) entryFor(entry.capletId).prompts = true; + for (const entry of manifest.completions) entryFor(entry.capletId).completions = true; + + const tools: RemoteCapletsTool[] = []; + const addPrimitiveTool = (capletId: string, operation: string) => { + const name = `${capletId}__${operation}`; + if (directToolNames.has(name)) return; + tools.push(primitiveTool(capletId, operation, codeModeMarker)); + }; + for (const [capletId, flags] of byCaplet) { + if (flags.resources) { + addPrimitiveTool(capletId, "list_resources"); + addPrimitiveTool(capletId, "read_resource"); + } + if (flags.resourceTemplates) { + addPrimitiveTool(capletId, "list_resource_templates"); + addPrimitiveTool(capletId, "read_resource"); + } + if (flags.prompts) { + addPrimitiveTool(capletId, "list_prompts"); + addPrimitiveTool(capletId, "get_prompt"); + } + if (flags.completions) { + addPrimitiveTool(capletId, "complete"); + } + } + return [...new Map(tools.map((tool) => [tool.name, tool])).values()]; +} + +function primitiveTool( + capletId: string, + operation: string, + codeModeMarker: Pick | Record, +): RemoteCapletsTool { + return { + name: `${capletId}__${operation}`, + capletId, + sourceCapletId: capletId, + title: operation, + description: `MCP ${operation.replace(/_/g, " ")}.`, + inputSchema: primitiveInputSchema(operation), + ...codeModeMarker, + }; +} + +function primitiveInputSchema(operation: string): Record { + if (operation === "read_resource") { + return { + type: "object", + properties: { uri: { type: "string" } }, + required: ["uri"], + additionalProperties: false, + }; + } + if (operation === "get_prompt") { + return { + type: "object", + properties: { + name: { type: "string" }, + args: { type: "object", additionalProperties: true }, + }, + required: ["name"], + additionalProperties: false, + }; + } + if (operation === "complete") { + return { + type: "object", + properties: { + ref: { type: "object", additionalProperties: true }, + argument: { type: "object", additionalProperties: true }, + }, + required: ["ref", "argument"], + additionalProperties: false, + }; + } + return { type: "object", additionalProperties: false }; +} + +function exportMapFor(manifest: AttachManifest): Map { + const mapped = new Map(); + const setIfAbsent = (key: string, entry: AttachManifestExport) => { + if (!mapped.has(key)) mapped.set(key, entry); + }; + for (const entry of manifest.caplets) { + mapped.set(entry.capletId, entry); + mapped.set(entry.name, entry); + } + for (const entry of manifest.tools) { + mapped.set(entry.name, entry); + } + for (const entry of manifest.codeModeCaplets) { + setIfAbsent(entry.capletId, entry); + setIfAbsent(entry.name, entry); + } + return mapped; +} + +function slashUrl(url: URL): URL { + const next = new URL(url); + next.pathname = next.pathname.endsWith("/") ? next.pathname : `${next.pathname}/`; + return next; +} + +function attachPayloadError(payload: unknown, status: number): Error { + const error = isPlainObject(payload) && isPlainObject(payload.error) ? payload.error : undefined; + const message = + typeof error?.message === "string" + ? error.message + : `Caplets attach invoke returned HTTP ${status}.`; + const thrown = new Error(message) as Error & { status?: number; code?: unknown }; + thrown.status = status; + if (error && "code" in error) thrown.code = error.code; + return thrown; +} + +function isAttachManifestStale(error: unknown): boolean { + return isPlainObject(error) && error.code === "ATTACH_MANIFEST_STALE"; +} + +function startAttachEvents( + attachUrl: URL, + requestInit: RequestInit | undefined, + fetchImpl: typeof fetch, + listeners: Set<() => void>, + onClose: (abort: AbortController, retry: boolean) => void, +): AbortController { + const abort = new AbortController(); + let retry = true; + void (async () => { + try { + const response = await fetchImpl(new URL("events", slashUrl(attachUrl)), { + ...requestInit, + method: "GET", + signal: abort.signal, + }); + if (!response.ok) { + retry = false; + return; + } + if (!response.body) { + retry = false; + return; + } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + while (!abort.signal.aborted) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const events = buffer.split("\n\n"); + buffer = events.pop() ?? ""; + for (const event of events) { + if (!event.includes("event: manifest_changed")) continue; + for (const listener of listeners) listener(); + } + } + } catch { + // Polling remains the fallback when the event stream is unavailable. + } finally { + onClose(abort, retry); + } + })(); + return abort; +} + +async function executeCodeModeRunRemote( + service: NativeCapletsService, + request: unknown, +): Promise { + const parsed = codeModeRunInputSchema.safeParse(request); + if (!parsed.success) { + return { + ok: false, + error: { + code: "REQUEST_INVALID", + message: "Code Mode run input is invalid.", + details: parsed.error.issues, + }, + diagnostics: [], + logs: { entries: [], truncated: false, stored: false }, + meta: { + runId: "", + traceId: "", + declarationHash: "", + durationMs: 0, + timeoutMs: 0, + maxTimeoutMs: 0, + }, + }; + } + return await runCodeMode({ + code: parsed.data.code, + service, + ...(parsed.data.timeoutMs === undefined ? {} : { timeoutMs: parsed.data.timeoutMs }), + runtimeScope: process.env.CAPLETS_MODE?.trim() || "remote", + }); +} + +function compatibleExport( + manifest: AttachManifest, + previous: AttachManifestExport, +): AttachManifestExport | undefined { + const next = [ + ...manifest.caplets, + ...manifest.tools, + ...manifest.resources, + ...manifest.resourceTemplates, + ...manifest.prompts, + ...manifest.completions, + ...manifest.codeModeCaplets, + ].find((entry) => entry.stableId === previous.stableId); + if (!next) return undefined; + if (next.kind !== previous.kind) return undefined; + if (next.schemaHash !== previous.schemaHash) return undefined; + return next; +} + +function promptArguments(inputSchema: unknown): { arguments: unknown[] } | Record { + if (!isPlainObject(inputSchema) || !Array.isArray(inputSchema.arguments)) return {}; + return { arguments: inputSchema.arguments }; +} + +function downstreamResourceUri(capletId: string, uri: string): string { + if (!uri.startsWith("caplets://")) return uri; + const decoded = decodeDirectResourceUri(uri); + if (decoded.capletId !== capletId) { + throw new CapletsError( + "ATTACH_EXPORT_NOT_FOUND", + "Attach resource URI belongs to a different Caplet.", + ); + } + return decoded.downstreamUri; +} + function operationNamesFromSchema(schema: Record): string[] { const properties = schema.properties; if (!isPlainObject(properties)) return [...operations]; @@ -285,8 +874,20 @@ function remoteAuthError(kind: "self_hosted_remote" | "hosted_cloud"): CapletsEr } function isSessionFailure(error: unknown): boolean { + const candidate = error as { code?: unknown; status?: unknown; statusCode?: unknown }; + if (candidate.status === 408 || candidate.statusCode === 408) return true; + if ( + typeof candidate.code === "string" && + /^(ECONNRESET|ECONNREFUSED|EPIPE|ETIMEDOUT|SESSION_EXPIRED|SESSION_CLOSED|TRANSPORT_CLOSED)$/u.test( + candidate.code, + ) + ) { + return true; + } const message = errorMessage(error).toLowerCase(); - return /session|transport|connection|connect|closed|invalid/u.test(message); + return /\b(invalid session|session (closed|expired|not found)|transport (connection )?closed|connection closed|closed connection|server unavailable|connection reset|econnreset|econnrefused)\b/u.test( + message, + ); } function isAuthFailure(error: unknown): boolean { diff --git a/packages/core/src/native/service.ts b/packages/core/src/native/service.ts index 41d711f3..aa1b01ca 100644 --- a/packages/core/src/native/service.ts +++ b/packages/core/src/native/service.ts @@ -59,6 +59,7 @@ export type NativeCapletsServiceOptions = NativeCapletsServiceResolutionInput & export type NativeCapletTool = { caplet: string; + sourceCaplet?: string; toolName: string; title: string; description: string; @@ -68,6 +69,7 @@ export type NativeCapletTool = { promptGuidance: string[]; inputSchema?: ReturnType | Record; outputSchema?: Record; + annotations?: Record; operationNames?: string[]; codeModeCaplets?: CodeModeCallableCaplet[]; }; @@ -301,6 +303,7 @@ class DefaultNativeCapletsService implements NativeCapletsService { promptGuidance: [`Use ${toolName} for ${caplet.name} ${operationName}.`], ...(options.inputSchema ? { inputSchema: options.inputSchema } : {}), ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}), + ...(options.annotations ? { annotations: options.annotations } : {}), }; } @@ -515,8 +518,11 @@ function codeModeCallableNativeTools( options: { fallbackToVisible: boolean }, ): NativeCapletTool[] { const codeModeCaplets = tools.flatMap((tool) => tool.codeModeCaplets ?? []); + const hasExplicitCodeModeManifest = tools.some((tool) => tool.codeModeCaplets !== undefined); if (codeModeCaplets.length === 0) { - return options.fallbackToVisible ? tools.filter((tool) => tool.codeModeRun !== true) : []; + return options.fallbackToVisible && !hasExplicitCodeModeManifest + ? tools.filter((tool) => tool.codeModeRun !== true) + : []; } const byId = new Map(tools.map((tool) => [tool.caplet, tool])); return codeModeCaplets.map((caplet) => { @@ -663,7 +669,7 @@ class CloudNativeCapletsService implements NativeCapletsService { } satisfies ResolvedNativeCloudPresenceOptions; const remoteOptions = { ...this.baseRemote, - url: selection.remote.mcpUrl, + url: selection.remote.attachUrl, auth: nativeAuthFromRemoteAuth(selection.remote.auth), requestInit: selection.remote.requestInit, ...(selection.remote.fetch ? { fetch: selection.remote.fetch } : {}), @@ -718,6 +724,7 @@ function nativeAuthFromRemoteAuth(auth: CapletsRemoteAuth): NativeRemoteAuthOpti class CompositeNativeCapletsService implements NativeCapletsService { private readonly listeners = new Set(); private readonly unsubscribers: Array<() => void>; + private readonly warnedShadowedLocalCaplets = new Set(); private tools: NativeCapletTool[] = []; private closed = false; private batchingReload = false; @@ -749,7 +756,9 @@ class CompositeNativeCapletsService implements NativeCapletsService { if (capletId === nativeCodeModeToolId) { return await executeCodeModeRunNative(this, request); } - if (this.local.listTools().some((tool) => tool.caplet === capletId)) { + const localHasCaplet = this.local.listTools().some((tool) => tool.caplet === capletId); + const remoteHasCaplet = serviceHasCaplet(this.remote, capletId); + if (localHasCaplet && !remoteHasCaplet) { return await this.local.execute(capletId, request); } return await this.remote.execute(capletId, request); @@ -813,17 +822,25 @@ class CompositeNativeCapletsService implements NativeCapletsService { private mergeTools(): NativeCapletTool[] { const allLocalTools = this.local.listTools(); const allRemoteTools = this.remote.listTools(); - const localTools = allLocalTools.filter((tool) => tool.codeModeRun !== true); - const localIds = new Set(localTools.map((tool) => tool.caplet)); - const localCodeModeTools = codeModeCallableNativeTools(allLocalTools, { - fallbackToVisible: false, - }); - const remoteTools = allRemoteTools.filter( - (tool) => tool.codeModeRun !== true && !localIds.has(tool.caplet), - ); const remoteCodeModeTools = codeModeCallableNativeTools(allRemoteTools, { fallbackToVisible: true, - }).filter((tool) => !localIds.has(tool.caplet)); + }); + const remoteIds = new Set( + [ + ...allRemoteTools + .filter((tool) => tool.codeModeRun !== true) + .map((tool) => tool.sourceCaplet ?? tool.caplet), + ...remoteCodeModeTools.map((tool) => tool.caplet), + ].filter((caplet) => caplet !== nativeCodeModeToolId), + ); + const localTools = allLocalTools.filter( + (tool) => tool.codeModeRun !== true && !remoteIds.has(tool.sourceCaplet ?? tool.caplet), + ); + this.warnShadowedLocalCaplets(allLocalTools, remoteIds); + const localCodeModeTools = codeModeCallableNativeTools(allLocalTools, { + fallbackToVisible: false, + }).filter((tool) => !remoteIds.has(tool.caplet)); + const remoteTools = allRemoteTools.filter((tool) => tool.codeModeRun !== true); const mergedTools = [...remoteTools, ...localTools]; const codeModeTools = [...remoteCodeModeTools, ...localCodeModeTools]; return [ @@ -832,6 +849,26 @@ class CompositeNativeCapletsService implements NativeCapletsService { ]; } + private warnShadowedLocalCaplets(localTools: NativeCapletTool[], remoteIds: Set): void { + const localIds = new Set([ + ...localTools + .filter((tool) => tool.codeModeRun !== true) + .map((tool) => tool.sourceCaplet ?? tool.caplet), + ...codeModeCallableNativeTools(localTools, { fallbackToVisible: false }).map( + (tool) => tool.caplet, + ), + ]); + for (const capletId of localIds) { + if (!remoteIds.has(capletId)) continue; + if (this.warnedShadowedLocalCaplets.has(capletId)) continue; + this.warnedShadowedLocalCaplets.add(capletId); + writeErr( + this.options, + `Local Caplet '${capletId}' is suppressed because the remote attach manifest forbids shadowing that Caplet ID.\n`, + ); + } + } + private async reloadChild( service: NativeCapletsService, label: string, @@ -848,6 +885,15 @@ class CompositeNativeCapletsService implements NativeCapletsService { } } +function serviceHasCaplet(service: NativeCapletsService, capletId: string): boolean { + return service.listTools().some((tool) => { + if (tool.codeModeRun) { + return tool.codeModeCaplets?.some((caplet) => caplet.id === capletId) ?? false; + } + return tool.caplet === capletId; + }); +} + function createProjectBindingSessionManager( cloud: ResolvedNativeRemoteOptions["cloud"], local: NativeCapletsService, diff --git a/packages/core/src/project-binding/routes.ts b/packages/core/src/project-binding/routes.ts index e0cbd839..cf4170f6 100644 --- a/packages/core/src/project-binding/routes.ts +++ b/packages/core/src/project-binding/routes.ts @@ -3,7 +3,7 @@ import { PROJECT_BINDING_STATES, type ProjectBindingState } from "./types"; export { PROJECT_BINDING_STATES }; export type { ProjectBindingState }; -export const PROJECT_BINDINGS_CONTROL_PATH = "/control/project-bindings"; +export const PROJECT_BINDINGS_CONTROL_PATH = "/v1/attach/project-bindings"; export const PROJECT_BINDING_CONNECT_PATH = `${PROJECT_BINDINGS_CONTROL_PATH}/connect`; export function projectBindingConnectPath(): string { diff --git a/packages/core/src/remote-control/dispatch.ts b/packages/core/src/remote-control/dispatch.ts index 59830024..52f28eb9 100644 --- a/packages/core/src/remote-control/dispatch.ts +++ b/packages/core/src/remote-control/dispatch.ts @@ -6,7 +6,13 @@ import { addMcpCaplet, addOpenApiCaplet, } from "./../cli/add"; -import { assertLoginTarget, findAuthTarget, listAuthRows, logoutAuthResult } from "./../cli/auth"; +import { + assertLoginTarget, + findAuthTarget, + listAuthRows, + logoutAuthResult, + refreshAuthResult, +} from "./../cli/auth"; import { completionShells, type CompletionShell } from "./../cli/completion"; import { initConfig } from "./../cli/init"; import { installCaplets } from "./../cli/install"; @@ -136,6 +142,13 @@ async function dispatch(request: RemoteCliRequest, context: RemoteControlDispatc }); } + if (request.command === "auth_refresh") { + return refreshAuthResult(requiredString(request.arguments, "server"), { + ...optionalProp("configPath", context.configPath), + ...optionalProp("authDir", context.authDir), + }); + } + if (request.command === "auth_login_start") { return startRemoteAuthLogin(requiredString(request.arguments, "server"), context); } diff --git a/packages/core/src/remote-control/types.ts b/packages/core/src/remote-control/types.ts index f26774d2..50981d15 100644 --- a/packages/core/src/remote-control/types.ts +++ b/packages/core/src/remote-control/types.ts @@ -23,6 +23,7 @@ export type RemoteCliCommand = | "auth_login_start" | "auth_login_complete" | "auth_logout" + | "auth_refresh" | "auth_list"; export type RemoteCliRequest = { diff --git a/packages/core/src/remote/options.ts b/packages/core/src/remote/options.ts index 2b4148c4..ff7fab24 100644 --- a/packages/core/src/remote/options.ts +++ b/packages/core/src/remote/options.ts @@ -39,6 +39,7 @@ export type CapletsRemoteAuth = export type ResolvedCapletsRemote = { baseUrl: URL; mcpUrl: URL; + attachUrl: URL; controlUrl: URL; healthUrl: URL; projectBindingWebSocketUrl: URL; @@ -140,9 +141,10 @@ export function resolveCapletsRemote( return { baseUrl, - mcpUrl: appendBasePath(baseUrl, "mcp"), - controlUrl: appendBasePath(baseUrl, "control"), - healthUrl: appendBasePath(baseUrl, "healthz"), + mcpUrl: appendBasePath(baseUrl, "v1/mcp"), + attachUrl: appendBasePath(baseUrl, "v1/attach"), + controlUrl: appendBasePath(baseUrl, "v1/admin"), + healthUrl: appendBasePath(baseUrl, "v1/healthz"), projectBindingWebSocketUrl: projectBindingWebSocketUrlForBase(baseUrl), auth, requestInit, @@ -162,7 +164,10 @@ export function resolveHostedCloudRemote( } const cloud = parseHostedCloudRemoteUrl(rawUrl); - const workspace = cloud.workspace ?? nonEmpty(input.workspace, "workspace"); + const workspace = + cloud.workspace ?? + nonEmpty(input.workspace, "workspace") ?? + nonEmpty(env.CAPLETS_REMOTE_WORKSPACE, "CAPLETS_REMOTE_WORKSPACE"); if (!workspace) { throw new CapletsError( "REQUEST_INVALID", @@ -177,14 +182,17 @@ export function resolveHostedCloudRemote( : { type: "none", user: DEFAULT_REMOTE_USER }; const requestInit: RequestInit = auth.type === "bearer" ? { headers: { Authorization: `Bearer ${auth.token}` } } : {}; - const workspaceBaseUrl = appendBasePath(cloud.baseUrl, `ws/${encodeURIComponent(workspace)}`); + const workspaceBaseUrl = appendBasePath(cloud.baseUrl, `v1/ws/${encodeURIComponent(workspace)}`); return { baseUrl: cloud.baseUrl, mcpUrl: appendBasePath(workspaceBaseUrl, "mcp"), - controlUrl: appendBasePath(cloud.baseUrl, "control"), - healthUrl: appendBasePath(cloud.baseUrl, "healthz"), - projectBindingWebSocketUrl: projectBindingWebSocketUrlForBase(cloud.baseUrl), + attachUrl: appendBasePath(workspaceBaseUrl, "attach"), + controlUrl: appendBasePath(cloud.baseUrl, "v1/admin"), + healthUrl: appendBasePath(cloud.baseUrl, "v1/healthz"), + projectBindingWebSocketUrl: webSocketUrl( + appendBasePath(workspaceBaseUrl, "attach/project-bindings/connect"), + ), auth, requestInit, workspace, @@ -201,7 +209,11 @@ export function hostedCloudWorkspaceFromRemoteUrl(value: string): string | undef } export function projectBindingWebSocketUrlForBase(baseUrl: URL): URL { - const url = appendBasePath(baseUrl, "control/project-bindings/connect"); + return webSocketUrl(appendBasePath(baseUrl, "v1/attach/project-bindings/connect")); +} + +function webSocketUrl(input: URL): URL { + const url = new URL(input); if (url.protocol === "https:") url.protocol = "wss:"; if (url.protocol === "http:") url.protocol = "ws:"; return url; @@ -231,7 +243,7 @@ function parseHostedCloudRemoteUrl(value: string): { baseUrl: URL; workspace?: s baseUrl.pathname = "/"; const pathname = url.pathname.replace(/\/+$/u, ""); if (pathname === "") return { baseUrl }; - const match = pathname.match(/^\/ws\/([^/]+)(?:\/mcp)?$/u); + const match = pathname.match(/^(?:\/v1)?\/ws\/([^/]+)(?:\/(?:mcp|attach))?$/u); if (!match) { throw new CapletsError( "REQUEST_INVALID", diff --git a/packages/core/src/serve/http.ts b/packages/core/src/serve/http.ts index 6ad8627e..63676436 100644 --- a/packages/core/src/serve/http.ts +++ b/packages/core/src/serve/http.ts @@ -7,6 +7,12 @@ import { logger } from "hono/logger"; import { resolveProjectCapletsRoot } from "../config"; import { CapletsEngine, type CapletsEngineOptions } from "../engine"; import { CapletsError, toSafeError } from "../errors"; +import { + attachErrorResponse, + buildAttachProjection, + invokeAttachExport, + type AttachInvokeRequest, +} from "../attach/api"; import { dispatchRemoteCliRequest, type RemoteControlDispatchContext, @@ -21,6 +27,7 @@ type HttpServeIo = { control?: Omit; authFlowStore?: RemoteAuthFlowStore; sessionFactory?: HttpMcpSessionFactory; + exposeAttach?: boolean; }; type HttpMcpSession = { @@ -39,6 +46,10 @@ export type CapletsHttpApp = Hono & { closeCapletsSessions: () => Promise; }; +type AttachEventStream = { + close: () => void; +}; + export function createHttpServeApp( options: HttpServeOptions, engine: CapletsEngine, @@ -46,9 +57,11 @@ export function createHttpServeApp( ): CapletsHttpApp { const app = new Hono() as CapletsHttpApp; const sessions = new Map(); + const attachEventStreams = new Set(); const writeErr = io.writeErr ?? process.stderr.write.bind(process.stderr); const paths = servicePaths(options.path); const authFlowStore = io.authFlowStore ?? new RemoteAuthFlowStore(); + const exposeAttach = io.exposeAttach ?? true; app.use( "*", logger((message, ...rest) => { @@ -61,21 +74,16 @@ export function createHttpServeApp( name: "caplets", transport: "http", base: paths.base, - mcp: paths.mcp, - control: paths.control, - health: paths.health, + versions: [versionDiscovery(paths, exposeAttach)], auth: { type: "basic", enabled: options.auth.enabled }, }), ); + app.get(paths.version, (c) => c.json(versionDiscovery(paths, exposeAttach))); + app.get(paths.health, (c) => c.json({ status: "ok", - transport: "http", - base: paths.base, - mcpPath: paths.mcp, - controlPath: paths.control, - healthPath: paths.health, }), ); @@ -124,6 +132,31 @@ export function createHttpServeApp( return session.transport.handleRequest(c); }); + const attachHostProtection = dnsRebindingProtection(options); + + if (exposeAttach) { + app.get(paths.attachManifest, attachHostProtection, basicAuth(options.auth), async (c) => { + const attachProjection = await buildAttachProjection(engine); + return c.json(attachProjection.manifest); + }); + + app.get(paths.attachEvents, attachHostProtection, basicAuth(options.auth), () => + attachEventsResponse(engine, attachEventStreams), + ); + + app.post(paths.attachInvoke, attachHostProtection, basicAuth(options.auth), async (c) => { + try { + const request = await parseAttachInvokeRequest(c.req.json()); + const attachProjection = await buildAttachProjection(engine); + const result = await invokeAttachExport(engine, attachProjection, request); + return c.json({ ok: true, data: result }); + } catch (error) { + const response = attachErrorResponse(error); + return c.json(response.body, response.status); + } + }); + } + app.post(paths.control, basicAuth(options.auth), async (c) => { let request: RemoteCliRequest; try { @@ -157,18 +190,40 @@ export function createHttpServeApp( ); }); - app.get(routePath(paths.control, "project-bindings/connect"), basicAuth(options.auth), (c) => + app.get(routePath(paths.projectBindings, "connect"), basicAuth(options.auth), (c) => c.json({ error: "websocket_upgrade_required" }, 426), ); - app.get( - routePath(paths.control, "project-bindings/:bindingId/status"), - basicAuth(options.auth), - (c) => - c.json({ - bindingId: c.req.param("bindingId"), - state: "not_attached", - }), + app.get(routePath(paths.projectBindings, ":bindingId/status"), basicAuth(options.auth), (c) => + c.json({ + bindingId: c.req.param("bindingId"), + state: "not_attached", + }), + ); + + app.post(routePath(paths.projectBindings, "sessions"), basicAuth(options.auth), (c) => { + const bindingId = randomUUID(); + return c.json( + { + binding: { bindingId, state: "attaching", syncState: "pending" }, + sessionId: randomUUID(), + }, + 201, + ); + }); + + app.post(routePath(paths.projectBindings, ":bindingId/heartbeat"), basicAuth(options.auth), (c) => + c.json({ + ok: true, + binding: { bindingId: c.req.param("bindingId"), state: "ready" }, + }), + ); + + app.delete(routePath(paths.projectBindings, ":bindingId/session"), basicAuth(options.auth), (c) => + c.json({ + ok: true, + binding: { bindingId: c.req.param("bindingId"), state: "ended" }, + }), ); app.get(routePath(paths.control, "auth/callback/:flowId"), async (c) => { @@ -197,6 +252,9 @@ export function createHttpServeApp( app.notFound((c) => c.json({ error: "not_found" }, 404)); app.closeCapletsSessions = async () => { + for (const stream of attachEventStreams) { + stream.close(); + } await Promise.allSettled( [...sessions.values()].map(async (session) => { await session.server.close(); @@ -259,6 +317,124 @@ function firstForwardedValue(value: string | undefined): string | undefined { return value?.split(",", 1)[0]?.trim() || undefined; } +function versionDiscovery(paths: ReturnType, exposeAttach = true) { + return { + version: 1, + path: paths.version, + links: { + mcp: paths.mcp, + admin: paths.control, + ...(exposeAttach + ? { + attachManifest: paths.attachManifest, + attachEvents: paths.attachEvents, + attachInvoke: paths.attachInvoke, + } + : {}), + health: paths.health, + }, + }; +} + +async function parseAttachInvokeRequest(input: Promise): Promise { + let parsed: unknown; + try { + parsed = await input; + } catch (error) { + throw new CapletsError( + "REQUEST_INVALID", + "Attach invoke request body must be valid JSON.", + error, + ); + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new CapletsError("REQUEST_INVALID", "Attach invoke request JSON must be an object."); + } + const request = parsed as Record; + if ( + typeof request.revision !== "string" || + typeof request.kind !== "string" || + typeof request.exportId !== "string" + ) { + throw new CapletsError( + "REQUEST_INVALID", + "Attach invoke request requires revision, kind, and exportId.", + ); + } + if (!isAttachExportKind(request.kind)) { + throw new CapletsError("REQUEST_INVALID", "Attach invoke kind is invalid."); + } + return { + revision: request.revision, + kind: request.kind, + exportId: request.exportId, + input: request.input, + }; +} + +function isAttachExportKind(value: string): value is AttachInvokeRequest["kind"] { + return ( + value === "caplet" || + value === "tool" || + value === "resource" || + value === "resourceTemplate" || + value === "prompt" || + value === "completion" + ); +} + +function attachEventsResponse( + engine: CapletsEngine, + activeStreams: Set, +): Response { + const encoder = new TextEncoder(); + let unsubscribe: () => void = () => undefined; + let activeStream: AttachEventStream | undefined; + let closed = false; + const readable = new ReadableStream({ + start(controller) { + activeStream = { + close: () => { + if (closed) return; + closed = true; + unsubscribe(); + if (activeStream) activeStreams.delete(activeStream); + try { + controller.close(); + } catch { + // The stream may already have been cancelled by the client. + } + }, + }; + activeStreams.add(activeStream); + controller.enqueue(encoder.encode(": connected\n\n")); + unsubscribe = engine.onReload(() => { + void buildAttachProjection(engine) + .then((projection) => { + if (closed) return; + controller.enqueue( + encoder.encode( + `event: manifest_changed\ndata: ${JSON.stringify({ revision: projection.manifest.revision })}\n\n`, + ), + ); + }) + .catch(() => undefined); + }); + }, + cancel() { + activeStream?.close(); + }, + }); + return new Response(readable, { + headers: { + "content-type": "text/event-stream", + "cache-control": "no-cache", + connection: "keep-alive", + "x-accel-buffering": "no", + }, + }); +} + export async function serveHttp( options: HttpServeOptions, engineOptions: CapletsEngineOptions = {}, @@ -278,6 +454,7 @@ export async function serveHttp( const server = serve({ fetch: app.fetch, hostname: options.host, port: options.port }, () => { writeErr(`Caplets HTTP service listening on ${baseUrl}\n`); writeErr(`MCP endpoint: ${origin}${paths.mcp}\n`); + writeErr(`Attach manifest: ${origin}${paths.attachManifest}\n`); writeErr(`Control endpoint: ${origin}${paths.control}\n`); writeErr(`Health check: ${origin}${paths.health}\n`); writeErr( @@ -296,6 +473,7 @@ export async function serveHttpWithSessionFactory( const engine = new CapletsEngine({}); const app = createHttpServeApp(options, engine, { writeErr, + exposeAttach: false, sessionFactory: createSession, control: { projectCapletsRoot: resolveProjectCapletsRoot(), @@ -333,15 +511,27 @@ export function routePath(base: string, path: string): string { export function servicePaths(base: string): { base: string; + version: string; mcp: string; control: string; + attachManifest: string; + attachEvents: string; + attachInvoke: string; + projectBindings: string; health: string; } { + const version = routePath(base, "v1"); + const attach = routePath(version, "attach"); return { base, - mcp: routePath(base, "mcp"), - control: routePath(base, "control"), - health: routePath(base, "healthz"), + version, + mcp: routePath(version, "mcp"), + control: routePath(version, "admin"), + attachManifest: routePath(attach, "manifest"), + attachEvents: routePath(attach, "events"), + attachInvoke: routePath(attach, "invoke"), + projectBindings: routePath(attach, "project-bindings"), + health: routePath(version, "healthz"), }; } @@ -381,6 +571,22 @@ function basicAuth(auth: HttpBasicAuthOptions): MiddlewareHandler { }; } +function dnsRebindingProtection(options: HttpServeOptions): MiddlewareHandler { + if (!options.loopback) { + return async (_c, next) => { + await next(); + }; + } + const allowedHosts = new Set(dnsRebindingOptions(options).allowedHosts); + return async (c, next) => { + const host = c.req.header("host"); + if (host && !allowedHosts.has(host)) { + return c.text("Forbidden", 403); + } + await next(); + }; +} + function parseBasicAuth(header: string): { user: string; password: string } | undefined { const [scheme, encoded] = header.split(" "); if (scheme?.toLocaleLowerCase() !== "basic" || !encoded) { @@ -433,8 +639,9 @@ function installHttpSignalHandlers( let closing: Promise | undefined; const close = async () => { closing ??= (async () => { - await new Promise((resolve) => server.close(() => resolve())); await app.closeCapletsSessions(); + closeAllServerConnections(server); + await new Promise((resolve) => server.close(() => resolve())); await engine.close(); })(); return closing; @@ -455,6 +662,11 @@ function installHttpSignalHandlers( ); } +function closeAllServerConnections(server: ServerType): void { + const closeAllConnections = (server as { closeAllConnections?: () => void }).closeAllConnections; + closeAllConnections?.call(server); +} + function formatHost(host: string): string { return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; } diff --git a/packages/core/src/serve/native-session.ts b/packages/core/src/serve/native-session.ts index 4ac37636..ddeff748 100644 --- a/packages/core/src/serve/native-session.ts +++ b/packages/core/src/serve/native-session.ts @@ -61,7 +61,8 @@ export class NativeCapletsMcpSession { this.server.registerTool( tool.caplet, this.definition(tool), - async (request: unknown) => (await this.service.execute(tool.caplet, request)) as never, + async (request: unknown) => + nativeToolResult(await this.service.execute(tool.caplet, request)) as never, ), ); } @@ -73,6 +74,7 @@ export class NativeCapletsMcpSession { title: tool.title, description: tool.description, inputSchema: isRecord(tool.inputSchema) ? jsonSchemaToZodShape(tool.inputSchema) : undefined, + ...(isRecord(tool.annotations) ? { annotations: tool.annotations } : {}), }; } } @@ -112,3 +114,27 @@ function jsonSchemaPropertyToZod(value: unknown): z.ZodTypeAny { function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } + +function nativeToolResult(result: unknown): { + content: Array<{ type: "text"; text: string }>; + structuredContent: unknown; + isError?: true; +} { + if (isCallToolResult(result)) { + return result; + } + const isError = isRecord(result) && (result.isError === true || result.ok === false); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + ...(isError ? { isError: true } : {}), + }; +} + +function isCallToolResult(value: unknown): value is { + content: Array<{ type: "text"; text: string }>; + structuredContent: unknown; + isError?: true; +} { + return isRecord(value) && Array.isArray(value.content); +} diff --git a/packages/core/src/server/options.ts b/packages/core/src/server/options.ts index bd45e45a..188add8d 100644 --- a/packages/core/src/server/options.ts +++ b/packages/core/src/server/options.ts @@ -37,6 +37,7 @@ export type CapletsServerAuth = export type ResolvedCapletsServer = { baseUrl: URL; mcpUrl: URL; + attachUrl: URL; controlUrl: URL; healthUrl: URL; auth: CapletsServerAuth; @@ -107,6 +108,7 @@ export function resolveCapletsServer( return { baseUrl, mcpUrl: mcpUrlForBase(baseUrl), + attachUrl: attachUrlForBase(baseUrl), controlUrl: controlUrlForBase(baseUrl), healthUrl: healthUrlForBase(baseUrl), auth, @@ -116,15 +118,19 @@ export function resolveCapletsServer( } export function mcpUrlForBase(baseUrl: URL): URL { - return appendBasePath(baseUrl, "mcp"); + return appendBasePath(baseUrl, "v1/mcp"); +} + +export function attachUrlForBase(baseUrl: URL): URL { + return appendBasePath(baseUrl, "v1/attach"); } export function controlUrlForBase(baseUrl: URL): URL { - return appendBasePath(baseUrl, "control"); + return appendBasePath(baseUrl, "v1/admin"); } export function healthUrlForBase(baseUrl: URL): URL { - return appendBasePath(baseUrl, "healthz"); + return appendBasePath(baseUrl, "v1/healthz"); } export function appendBasePath(baseUrl: URL, path: string): URL { diff --git a/packages/core/test/attach-api.test.ts b/packages/core/test/attach-api.test.ts new file mode 100644 index 00000000..8888cc1e --- /dev/null +++ b/packages/core/test/attach-api.test.ts @@ -0,0 +1,577 @@ +import { describe, expect, it, vi } from "vitest"; +import { + buildAttachProjection, + invokeAttachExport, + type AttachProjection, +} from "../src/attach/api"; +import type { CapletsEngine } from "../src/engine"; + +describe("Attach API dispatch", () => { + it("sorts attach exports before hashing revisions", async () => { + const caplet = { + server: "docs", + name: "Docs", + description: "Docs.", + backend: "mcp", + command: process.execPath, + }; + let reversed = false; + const tools = [ + { + caplet, + downstreamName: "beta", + name: "docs__beta", + tool: { name: "beta", inputSchema: { type: "object" } }, + }, + { + caplet, + downstreamName: "alpha", + name: "docs__alpha", + tool: { name: "alpha", inputSchema: { type: "object" } }, + }, + ]; + const engine = { + exposureSnapshot: async () => { + reversed = !reversed; + return { + callableCaplets: [], + progressiveCaplets: [], + codeModeCaplets: [], + directTools: reversed ? tools : [...tools].reverse(), + directResources: [], + directResourceTemplates: [], + directPrompts: [], + hiddenCaplets: [], + }; + }, + } as unknown as CapletsEngine; + + const first = await buildAttachProjection(engine); + const second = await buildAttachProjection(engine); + + expect(first.manifest.revision).toBe(second.manifest.revision); + expect(first.manifest.tools.map((tool) => tool.stableId)).toEqual([ + "tool:docs:alpha", + "tool:docs:beta", + ]); + }); + + it("preserves direct tool annotations in attach manifests", async () => { + const caplet = { + server: "docs", + name: "Docs", + description: "Docs.", + backend: "mcp", + command: process.execPath, + }; + const engine = { + exposureSnapshot: async () => ({ + callableCaplets: [], + progressiveCaplets: [], + codeModeCaplets: [], + directTools: [ + { + caplet, + downstreamName: "delete", + name: "docs__delete", + tool: { + name: "delete", + inputSchema: { type: "object" }, + annotations: { destructiveHint: true }, + }, + }, + ], + directResources: [], + directResourceTemplates: [], + directPrompts: [], + hiddenCaplets: [], + }), + } as unknown as CapletsEngine; + + const projection = await buildAttachProjection(engine); + + expect(projection.manifest.tools).toEqual([ + expect.objectContaining({ + annotations: { destructiveHint: true }, + }), + ]); + }); + + it("preserves direct resource metadata in attach manifests", async () => { + const caplet = { + server: "docs", + name: "Docs", + description: "Docs.", + backend: "mcp", + command: process.execPath, + }; + const engine = { + exposureSnapshot: async () => ({ + callableCaplets: [], + progressiveCaplets: [], + codeModeCaplets: [], + directTools: [], + directResources: [ + { + caplet, + downstreamUri: "file:///README.md", + uri: "caplets://docs/resources/file%3A%2F%2F%2FREADME.md", + resource: { + uri: "file:///README.md", + name: "README", + description: "README resource.", + mimeType: "text/markdown", + size: 42, + }, + }, + ], + directResourceTemplates: [ + { + caplet, + downstreamUriTemplate: "file:///{path}", + uriTemplate: + "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2F%7Bpath%7D", + resourceTemplate: { + uriTemplate: "file:///{path}", + name: "File", + description: "File resource.", + mimeType: "text/plain", + }, + }, + ], + directPrompts: [], + hiddenCaplets: [], + }), + } as unknown as CapletsEngine; + + const projection = await buildAttachProjection(engine); + + expect(projection.manifest.resources).toEqual([ + expect.objectContaining({ + mimeType: "text/markdown", + size: 42, + }), + ]); + expect(projection.manifest.resourceTemplates).toEqual([ + expect.objectContaining({ + mimeType: "text/plain", + }), + ]); + }); + + it("reads resource template exports from an explicit expanded URI", async () => { + const engine = { + execute: vi.fn(async () => ({ ok: true })), + readDirectResource: vi.fn(async () => ({ contents: [{ uri: "file:///README.md" }] })), + } as unknown as CapletsEngine; + const projection = { + manifest: { + version: 1, + revision: "rev-1", + generatedAt: new Date(0).toISOString(), + caplets: [], + tools: [], + resources: [], + resourceTemplates: [ + { + stableId: "resourceTemplate:docs:file:///{path}", + exportId: "export-resource-template", + kind: "resourceTemplate", + uriTemplate: "caplets://docs/{path}", + downstreamUriTemplate: "file:///{path}", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + prompts: [], + completions: [], + codeModeCaplets: [], + diagnostics: [], + }, + routes: new Map([ + [ + "export-resource-template", + { + kind: "resourceTemplate", + capletId: "docs", + downstreamUriTemplate: "file:///{path}", + }, + ], + ]), + } satisfies AttachProjection; + + await expect( + invokeAttachExport(engine, projection, { + revision: "rev-1", + kind: "resourceTemplate", + exportId: "export-resource-template", + input: { uri: "file:///README.md" }, + }), + ).resolves.toEqual({ contents: [{ uri: "file:///README.md" }] }); + expect(engine.execute).not.toHaveBeenCalled(); + expect(engine.readDirectResource).toHaveBeenCalledWith("docs", "file:///README.md"); + }); + + it("decodes advertised resource template wrapper URIs before downstream reads", async () => { + const engine = { + execute: vi.fn(async () => ({ ok: true })), + readDirectResource: vi.fn(async () => ({ contents: [{ uri: "file:///README.md" }] })), + } as unknown as CapletsEngine; + const projection = { + manifest: { + version: 1, + revision: "rev-1", + generatedAt: new Date(0).toISOString(), + caplets: [], + tools: [], + resources: [], + resourceTemplates: [ + { + stableId: "resourceTemplate:docs:file:///{path}", + exportId: "export-resource-template", + kind: "resourceTemplate", + uriTemplate: + "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2F%7Bpath%7D", + downstreamUriTemplate: "file:///{path}", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + prompts: [], + completions: [], + codeModeCaplets: [], + diagnostics: [], + }, + routes: new Map([ + [ + "export-resource-template", + { + kind: "resourceTemplate", + capletId: "docs", + downstreamUriTemplate: "file:///{path}", + }, + ], + ]), + } satisfies AttachProjection; + + await invokeAttachExport(engine, projection, { + revision: "rev-1", + kind: "resourceTemplate", + exportId: "export-resource-template", + input: { uri: "caplets://docs/resources/file%3A%2F%2F%2FREADME.md" }, + }); + + expect(engine.readDirectResource).toHaveBeenCalledWith("docs", "file:///README.md"); + }); + + it("rejects resource template reads outside the advertised template", async () => { + const engine = { + execute: vi.fn(async () => ({ ok: true })), + readDirectResource: vi.fn(async () => ({ contents: [{ uri: "secrets:///README.md" }] })), + } as unknown as CapletsEngine; + const projection = { + manifest: { + version: 1, + revision: "rev-1", + generatedAt: new Date(0).toISOString(), + caplets: [], + tools: [], + resources: [], + resourceTemplates: [ + { + stableId: "resourceTemplate:docs:file:///{path}", + exportId: "export-resource-template", + kind: "resourceTemplate", + uriTemplate: + "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2F%7Bpath%7D", + downstreamUriTemplate: "file:///{path}", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + prompts: [], + completions: [], + codeModeCaplets: [], + diagnostics: [], + }, + routes: new Map([ + [ + "export-resource-template", + { + kind: "resourceTemplate", + capletId: "docs", + downstreamUriTemplate: "file:///{path}", + }, + ], + ]), + } satisfies AttachProjection; + + await expect( + invokeAttachExport(engine, projection, { + revision: "rev-1", + kind: "resourceTemplate", + exportId: "export-resource-template", + input: { uri: "secrets:///README.md" }, + }), + ).rejects.toMatchObject({ + code: "ATTACH_EXPORT_NOT_FOUND", + }); + expect(engine.readDirectResource).not.toHaveBeenCalled(); + }); + + it("returns request errors for malformed wrapped resource template URIs", async () => { + const engine = { + execute: vi.fn(async () => ({ ok: true })), + readDirectResource: vi.fn(async () => ({ contents: [] })), + } as unknown as CapletsEngine; + const projection = { + manifest: { + version: 1, + revision: "rev-1", + generatedAt: new Date(0).toISOString(), + caplets: [], + tools: [], + resources: [], + resourceTemplates: [ + { + stableId: "resourceTemplate:docs:file:///{path}", + exportId: "export-resource-template", + kind: "resourceTemplate", + uriTemplate: + "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2F%7Bpath%7D", + downstreamUriTemplate: "file:///{path}", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + prompts: [], + completions: [], + codeModeCaplets: [], + diagnostics: [], + }, + routes: new Map([ + [ + "export-resource-template", + { + kind: "resourceTemplate", + capletId: "docs", + downstreamUriTemplate: "file:///{path}", + }, + ], + ]), + } satisfies AttachProjection; + + await expect( + invokeAttachExport(engine, projection, { + revision: "rev-1", + kind: "resourceTemplate", + exportId: "export-resource-template", + input: { uri: "caplets://docs/resources/%E0%A4%A" }, + }), + ).rejects.toMatchObject({ + code: "REQUEST_INVALID", + }); + expect(engine.readDirectResource).not.toHaveBeenCalled(); + }); + + it("normalizes prompt completion refs to downstream prompt names", async () => { + const engine = { + execute: vi.fn(async () => ({ completion: "ok" })), + } as unknown as CapletsEngine; + const projection = { + manifest: { + version: 1, + revision: "rev-1", + generatedAt: new Date(0).toISOString(), + caplets: [], + tools: [], + resources: [], + resourceTemplates: [], + prompts: [ + { + stableId: "prompt:docs:review", + exportId: "export-prompt", + kind: "prompt", + name: "docs__review", + downstreamName: "review", + title: "Review", + description: "Review prompt.", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + completions: [ + { + stableId: "completion:docs", + exportId: "export-completion", + kind: "completion", + name: "docs__complete", + capletId: "docs", + schemaHash: null, + shadowing: "forbid", + }, + ], + codeModeCaplets: [], + diagnostics: [], + }, + routes: new Map([ + [ + "export-completion", + { + kind: "completion", + capletId: "docs", + }, + ], + ]), + } satisfies AttachProjection; + + await invokeAttachExport(engine, projection, { + revision: "rev-1", + kind: "completion", + exportId: "export-completion", + input: { + ref: { type: "prompt", name: "docs__review" }, + argument: { name: "topic", value: "attach" }, + }, + }); + + expect(engine.execute).toHaveBeenCalledWith("docs", { + operation: "complete", + ref: { type: "prompt", name: "review" }, + argument: { name: "topic", value: "attach" }, + }); + }); + + it("normalizes resource template completion refs to downstream templates", async () => { + const engine = { + execute: vi.fn(async () => ({ completion: "ok" })), + } as unknown as CapletsEngine; + const projection = { + manifest: { + version: 1, + revision: "rev-1", + generatedAt: new Date(0).toISOString(), + caplets: [], + tools: [], + resources: [], + resourceTemplates: [ + { + stableId: "resourceTemplate:docs:file:///{path}", + exportId: "export-resource-template", + kind: "resourceTemplate", + uriTemplate: + "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2F%7Bpath%7D", + downstreamUriTemplate: "file:///{path}", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + prompts: [], + completions: [ + { + stableId: "completion:docs", + exportId: "export-completion", + kind: "completion", + name: "docs__complete", + capletId: "docs", + schemaHash: null, + shadowing: "forbid", + }, + ], + codeModeCaplets: [], + diagnostics: [], + }, + routes: new Map([ + [ + "export-completion", + { + kind: "completion", + capletId: "docs", + }, + ], + ]), + } satisfies AttachProjection; + + await invokeAttachExport(engine, projection, { + revision: "rev-1", + kind: "completion", + exportId: "export-completion", + input: { + ref: { + type: "resourceTemplate", + uri: "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2F%7Bpath%7D", + }, + argument: { name: "path", value: "README.md" }, + }, + }); + + expect(engine.execute).toHaveBeenCalledWith("docs", { + operation: "complete", + ref: { type: "resourceTemplate", uri: "file:///{path}" }, + argument: { name: "path", value: "README.md" }, + }); + }); + + it("keeps completion invokes on the completion operation", async () => { + const engine = { + execute: vi.fn(async () => ({ completion: "ok" })), + } as unknown as CapletsEngine; + const projection = { + manifest: { + version: 1, + revision: "rev-1", + generatedAt: new Date(0).toISOString(), + caplets: [], + tools: [], + resources: [], + resourceTemplates: [], + prompts: [], + completions: [ + { + stableId: "completion:docs", + exportId: "export-completion", + kind: "completion", + name: "docs__complete", + capletId: "docs", + schemaHash: null, + shadowing: "forbid", + }, + ], + codeModeCaplets: [], + diagnostics: [], + }, + routes: new Map([ + [ + "export-completion", + { + kind: "completion", + capletId: "docs", + }, + ], + ]), + } satisfies AttachProjection; + + await invokeAttachExport(engine, projection, { + revision: "rev-1", + kind: "completion", + exportId: "export-completion", + input: { + operation: "read_resource", + ref: { type: "prompt", name: "review" }, + argument: { name: "topic", value: "attach" }, + }, + }); + + expect(engine.execute).toHaveBeenCalledWith("docs", { + operation: "complete", + ref: { type: "prompt", name: "review" }, + argument: { name: "topic", value: "attach" }, + }); + }); +}); diff --git a/packages/core/test/attach-cli.test.ts b/packages/core/test/attach-cli.test.ts index 8f8363e2..9e8c2347 100644 --- a/packages/core/test/attach-cli.test.ts +++ b/packages/core/test/attach-cli.test.ts @@ -47,6 +47,37 @@ describe("caplets attach CLI", () => { }); }); + it("treats attach --user and --password as remote Basic Auth for stdio serving", async () => { + const served: unknown[] = []; + await runCli( + [ + "attach", + "--remote-url", + "https://caplets.example.com/caplets", + "--user", + "alice", + "--password", + "secret", + ], + { + env: { CAPLETS_MODE: "remote" }, + attachServe: async (options: unknown) => { + served.push(options); + }, + } as never, + ); + + expect(served).toHaveLength(1); + expect(served[0]).toMatchObject({ + transport: "stdio", + selection: { + remote: { + auth: { type: "basic", user: "alice", password: "secret" }, + }, + }, + }); + }); + it("rejects attach server in local mode", async () => { await expect( runCli(["attach"], { @@ -106,9 +137,9 @@ describe("caplets attach CLI", () => { }), ).resolves.toMatchObject({ ok: true, - webSocketUrl: "ws://127.0.0.1:8787/caplets/control/project-bindings/connect", + webSocketUrl: "ws://127.0.0.1:8787/caplets/v1/attach/project-bindings/connect", }); - expect(requestedUrl).toBe("http://127.0.0.1:8787/caplets/control/project-bindings/connect"); + expect(requestedUrl).toBe("http://127.0.0.1:8787/caplets/v1/attach/project-bindings/connect"); }); it("probes the Cloud control route when given a copied Cloud MCP endpoint", async () => { @@ -139,10 +170,11 @@ describe("caplets attach CLI", () => { ), ).resolves.toMatchObject({ ok: true, - webSocketUrl: "wss://cloud.pr-2.preview.caplets.dev/control/project-bindings/connect", + webSocketUrl: + "wss://cloud.pr-2.preview.caplets.dev/v1/ws/personal-c9b49d/attach/project-bindings/connect", }); expect(requestedUrl).toBe( - "https://cloud.pr-2.preview.caplets.dev/control/project-bindings/connect", + "https://cloud.pr-2.preview.caplets.dev/v1/ws/personal-c9b49d/attach/project-bindings/connect", ); }); @@ -163,7 +195,7 @@ describe("caplets attach CLI", () => { } expect(out.join("")).toContain( - "Project Binding available at wss://caplets.example.com/caplets/control/project-bindings/connect.", + "Project Binding available at wss://caplets.example.com/caplets/v1/attach/project-bindings/connect.", ); }); diff --git a/packages/core/test/attach-server.test.ts b/packages/core/test/attach-server.test.ts index db6eaa2c..79e30a07 100644 --- a/packages/core/test/attach-server.test.ts +++ b/packages/core/test/attach-server.test.ts @@ -27,7 +27,12 @@ describe("NativeCapletsMcpSession", () => { operationNames: ["inspect"], }, ], - execute: vi.fn(async () => ({ ok: true })), + execute: vi.fn(async () => ({ + ok: true, + value: { smoke: true }, + diagnostics: [], + logs: { entries: [], truncated: false, stored: false }, + })), reload: vi.fn(async () => true), onToolsChanged: vi.fn(() => () => undefined), close: vi.fn(async () => undefined), @@ -39,12 +44,125 @@ describe("NativeCapletsMcpSession", () => { const tool = registered.get("remote-alpha") as { callback: (request: unknown) => Promise; }; - await expect(tool.callback({ operation: "inspect" })).resolves.toEqual({ ok: true }); + const result = await tool.callback({ operation: "inspect" }); + const envelope = { + ok: true, + value: { smoke: true }, + diagnostics: [], + logs: { entries: [], truncated: false, stored: false }, + }; + expect(result).toEqual({ + content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }], + structuredContent: envelope, + }); expect(service.execute).toHaveBeenCalledWith("remote-alpha", { operation: "inspect" }); await session.close(); expect(service.close).toHaveBeenCalledOnce(); }); + it("marks native tool envelopes with ok false as MCP errors", async () => { + const registered = new Map(); + const server = { + registerTool: vi.fn((name: string, definition: unknown, callback: unknown) => { + registered.set(name, { definition, callback }); + return { remove: vi.fn(), update: vi.fn() }; + }), + connect: vi.fn(async () => undefined), + close: vi.fn(async () => undefined), + }; + const envelope = { + ok: false, + error: { code: "REQUEST_INVALID", message: "Bad request" }, + diagnostics: [], + }; + const service = { + listTools: () => [{ caplet: "code_mode", title: "Code Mode", description: "Code Mode" }], + execute: vi.fn(async () => envelope), + reload: vi.fn(async () => true), + onToolsChanged: vi.fn(() => () => undefined), + close: vi.fn(async () => undefined), + }; + + const session = new NativeCapletsMcpSession(service as never, { server: server as never }); + + const tool = registered.get("code_mode") as { + callback: (request: unknown) => Promise; + }; + await expect(tool.callback({})).resolves.toEqual({ + content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }], + structuredContent: envelope, + isError: true, + }); + await session.close(); + }); + + it("passes through native tool results that are already MCP call results", async () => { + const registered = new Map(); + const server = { + registerTool: vi.fn((name: string, definition: unknown, callback: unknown) => { + registered.set(name, { definition, callback }); + return { remove: vi.fn(), update: vi.fn() }; + }), + connect: vi.fn(async () => undefined), + close: vi.fn(async () => undefined), + }; + const callResult = { + content: [{ type: "text", text: "hello" }], + structuredContent: { message: "hello" }, + }; + const service = { + listTools: () => [{ caplet: "alpha", title: "Alpha", description: "Alpha" }], + execute: vi.fn(async () => callResult), + reload: vi.fn(async () => true), + onToolsChanged: vi.fn(() => () => undefined), + close: vi.fn(async () => undefined), + }; + + const session = new NativeCapletsMcpSession(service as never, { server: server as never }); + + const tool = registered.get("alpha") as { + callback: (request: unknown) => Promise; + }; + await expect(tool.callback({})).resolves.toBe(callResult); + await session.close(); + }); + + it("registers native tool annotations with MCP clients", async () => { + const registered = new Map(); + const server = { + registerTool: vi.fn((name: string, definition: unknown, callback: unknown) => { + registered.set(name, { definition, callback }); + return { remove: vi.fn(), update: vi.fn() }; + }), + connect: vi.fn(async () => undefined), + close: vi.fn(async () => undefined), + }; + const service = { + listTools: () => [ + { + caplet: "alpha", + title: "Alpha", + description: "Alpha", + annotations: { readOnlyHint: true }, + }, + ], + execute: vi.fn(async () => ({})), + reload: vi.fn(async () => true), + onToolsChanged: vi.fn(() => () => undefined), + close: vi.fn(async () => undefined), + }; + + const session = new NativeCapletsMcpSession(service as never, { server: server as never }); + + expect(registered.get("alpha")).toEqual({ + definition: expect.objectContaining({ + annotations: { readOnlyHint: true }, + }), + callback: expect.any(Function), + }); + await session.close(); + }); + it("updates registered tools when the native service changes", () => { let listener: ((tools: unknown[]) => void) | undefined; const removed = vi.fn(); diff --git a/packages/core/test/auth.test.ts b/packages/core/test/auth.test.ts index bb46645f..4fad4e48 100644 --- a/packages/core/test/auth.test.ts +++ b/packages/core/test/auth.test.ts @@ -820,6 +820,152 @@ describe("auth helpers", () => { }, ); + it("persists dynamically registered MCP OAuth client information with saved tokens", () => { + const dir = mkdtempSync(join(tmpdir(), "caplets-auth-dynamic-client-")); + try { + const server = parseConfig({ + mcpServers: { + remote: { + name: "Remote", + description: "A useful remote server.", + transport: "http", + url: "https://example.com/mcp", + auth: { type: "oauth2" }, + }, + }, + }).mcpServers.remote!; + const provider = new FileOAuthProvider(server, "http://127.0.0.1/callback", () => {}, dir); + provider.saveClientInformation({ + client_id: "dynamic-client", + client_secret: "dynamic-secret", + }); + + provider.saveTokens({ + access_token: "access-token", + refresh_token: "refresh-token", + token_type: "Bearer", + expires_in: 3600, + }); + + expect(readTokenBundle("remote", dir)).toMatchObject({ + clientId: "dynamic-client", + clientSecret: "dynamic-secret", + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("uses stored MCP OAuth client information from a fresh provider", async () => { + const dir = mkdtempSync(join(tmpdir(), "caplets-auth-stored-client-")); + try { + const server = parseConfig({ + mcpServers: { + remote: { + name: "Remote", + description: "A useful remote server.", + transport: "http", + url: "https://example.com/mcp", + auth: { type: "oauth2" }, + }, + }, + }).mcpServers.remote!; + writeTokenBundle( + { + server: "remote", + accessToken: "access-token", + refreshToken: "refresh-token", + clientId: "stored-client", + clientSecret: "stored-secret", + }, + dir, + ); + const provider = new FileOAuthProvider(server, "http://127.0.0.1/callback", () => {}, dir); + const headers = new Headers(); + const params = new URLSearchParams(); + + await provider.addClientAuthentication(headers, params); + + expect(params.get("client_id")).toBe("stored-client"); + expect(params.get("client_secret")).toBe("stored-secret"); + expect(headers.get("content-type")).toBe("application/x-www-form-urlencoded"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("explains that older dynamic MCP OAuth credentials need a fresh login", async () => { + const dir = mkdtempSync(join(tmpdir(), "caplets-auth-missing-client-")); + try { + const server = parseConfig({ + mcpServers: { + remote: { + name: "Remote", + description: "A useful remote server.", + transport: "http", + url: "https://example.com/mcp", + auth: { + type: "oauth2", + tokenUrl: "https://auth.example.com/token", + }, + }, + }, + }).mcpServers.remote!; + writeTokenBundle( + { + server: "remote", + accessToken: "access-token", + refreshToken: "refresh-token", + expiresAt: "2000-01-01T00:00:00.000Z", + }, + dir, + ); + + await expect(oauthHeaders(server, dir)).rejects.toThrow( + "OAuth client information is missing for remote. Re-run caplets auth login remote.", + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("ignores legacy dynamic MCP OAuth tokens without client information during login", async () => { + const dir = mkdtempSync(join(tmpdir(), "caplets-auth-legacy-login-")); + try { + const server = parseConfig({ + mcpServers: { + remote: { + name: "Remote", + description: "A useful remote server.", + transport: "http", + url: "https://example.com/mcp", + auth: { type: "oauth2" }, + }, + }, + }).mcpServers.remote!; + writeTokenBundle( + { + server: "remote", + accessToken: "old-access-token", + refreshToken: "old-refresh-token", + expiresAt: "2000-01-01T00:00:00.000Z", + }, + dir, + ); + mockMcpAuth.mockClear(); + mockMcpAuth.mockImplementationOnce(async (provider: FileOAuthProvider) => { + expect(await provider.tokens()).toBeUndefined(); + return "AUTHORIZED"; + }); + + await runOAuthFlow(server, { noOpen: true, authDir: dir }); + + expect(mockMcpAuth).toHaveBeenCalledOnce(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("does not mix dynamic public client ID with configured client secret", async () => { const server = parseConfig({ mcpServers: { diff --git a/packages/core/test/cli-completion.test.ts b/packages/core/test/cli-completion.test.ts index 226488df..16f398bd 100644 --- a/packages/core/test/cli-completion.test.ts +++ b/packages/core/test/cli-completion.test.ts @@ -69,6 +69,12 @@ describe("CLI completion resolver", () => { "graphql", "http", ]); + await expect(completeCliWords(["auth", ""])).resolves.toEqual([ + "login", + "logout", + "list", + "refresh", + ]); await expect(completeCliWords(["completion", ""])).resolves.toEqual([ "bash", "zsh", diff --git a/packages/core/test/cli-remote.test.ts b/packages/core/test/cli-remote.test.ts index 8eae29bc..20b07bd6 100644 --- a/packages/core/test/cli-remote.test.ts +++ b/packages/core/test/cli-remote.test.ts @@ -188,7 +188,7 @@ describe("remote CLI routing", () => { expect(requests).toEqual([ { - url: "http://127.0.0.1:5387/caplets/control", + url: "http://127.0.0.1:5387/caplets/v1/admin", body: JSON.stringify({ command: "list", arguments: { includeDisabled: false } }), }, ]); @@ -389,7 +389,7 @@ describe("remote CLI routing", () => { expect(requests).toEqual([ { - url: "http://127.0.0.1:5387/caplets/control", + url: "http://127.0.0.1:5387/caplets/v1/admin", body: JSON.stringify({ command: "call_tool", arguments: { @@ -735,7 +735,7 @@ describe("remote CLI routing", () => { expect(out.join("")).toBe("Wrote remote MCP Caplet to /srv/caplets/.caplets/github.md\n"); expect(fetchMock).toHaveBeenCalledWith( - new URL("http://127.0.0.1:5387/control"), + new URL("http://127.0.0.1:5387/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "add", @@ -985,7 +985,7 @@ describe("remote CLI routing", () => { expect(out.join("")).toBe("Installed github to remote /srv/caplets/.caplets/github\n"); expect(fetchMock).toHaveBeenCalledWith( - new URL("http://127.0.0.1:5387/control"), + new URL("http://127.0.0.1:5387/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "install", @@ -1072,7 +1072,7 @@ describe("remote CLI routing", () => { expect(out.join("")).toBe("Created remote Caplets config at /srv/caplets/config.json\n"); expect(fetchMock).toHaveBeenCalledWith( - new URL("http://127.0.0.1:5387/control"), + new URL("http://127.0.0.1:5387/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "init", arguments: { force: true } }), }), @@ -1154,14 +1154,14 @@ describe("remote CLI routing", () => { expect(out[1]).toBe("Deleted remote OAuth credentials for `remote`.\n"); expect(fetchMock).toHaveBeenNthCalledWith( 1, - new URL("http://127.0.0.1:5387/control"), + new URL("http://127.0.0.1:5387/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "auth_list", arguments: {} }), }), ); expect(fetchMock).toHaveBeenNthCalledWith( 2, - new URL("http://127.0.0.1:5387/control"), + new URL("http://127.0.0.1:5387/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "auth_logout", arguments: { server: "remote" } }), }), @@ -1187,7 +1187,7 @@ describe("remote CLI routing", () => { expect.objectContaining({ server: "remote-auth", source: "remote" }), ]); expect(fetchMock).toHaveBeenCalledWith( - new URL("http://127.0.0.1:5387/caplets/control"), + new URL("http://127.0.0.1:5387/caplets/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "auth_list", arguments: {} }) }), ); }); @@ -1208,7 +1208,7 @@ describe("remote CLI routing", () => { ).rejects.toThrow(/--project.*--global.*--remote/s); expect(fetchMock).toHaveBeenCalledWith( - new URL("http://127.0.0.1:5387/caplets/control"), + new URL("http://127.0.0.1:5387/caplets/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "auth_list", arguments: {} }) }), ); }); @@ -1231,13 +1231,14 @@ describe("remote CLI routing", () => { expect(fetchMock).not.toHaveBeenCalled(); }); - it("uses explicit --remote for auth login and logout remote requests", async () => { + it("uses explicit --remote for auth login, refresh, and logout remote requests", async () => { const out: string[] = []; const fetchMock = vi .fn() .mockResolvedValueOnce( Response.json({ ok: true, result: { server: "remote", authenticated: true } }), ) + .mockResolvedValueOnce(Response.json({ ok: true, result: { server: "remote" } })) .mockResolvedValueOnce( Response.json({ ok: true, result: { server: "remote", deleted: true } }), ); @@ -1252,22 +1253,31 @@ describe("remote CLI routing", () => { }; await runCli(["auth", "login", "remote", "--remote", "--no-open"], io); + await runCli(["auth", "refresh", "remote", "--remote"], io); await runCli(["auth", "logout", "remote", "--remote"], io); expect(fetchMock).toHaveBeenNthCalledWith( 1, - new URL("http://127.0.0.1:5387/control"), + new URL("http://127.0.0.1:5387/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "auth_login_start", arguments: { server: "remote" } }), }), ); expect(fetchMock).toHaveBeenNthCalledWith( 2, - new URL("http://127.0.0.1:5387/control"), + new URL("http://127.0.0.1:5387/v1/admin"), + expect.objectContaining({ + body: JSON.stringify({ command: "auth_refresh", arguments: { server: "remote" } }), + }), + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + new URL("http://127.0.0.1:5387/v1/admin"), expect.objectContaining({ body: JSON.stringify({ command: "auth_logout", arguments: { server: "remote" } }), }), ); + expect(out).toContain("Refreshed remote OAuth credentials for `remote`.\n"); }); it("does not print or open an auth URL when remote auth is already complete", async () => { @@ -1344,7 +1354,7 @@ async function runRemoteAdd(args: string[]): Promise { }); expect(requests).toHaveLength(1); - expect(requests[0]).toMatchObject({ url: "http://127.0.0.1:5387/control" }); + expect(requests[0]).toMatchObject({ url: "http://127.0.0.1:5387/v1/admin" }); return JSON.parse((requests[0] as { body: string }).body); } diff --git a/packages/core/test/cli.test.ts b/packages/core/test/cli.test.ts index 1614f4d4..c2288a40 100644 --- a/packages/core/test/cli.test.ts +++ b/packages/core/test/cli.test.ts @@ -15,7 +15,7 @@ import { version as packageJsonVersion } from "../package.json"; import { initConfig, installCaplets, normalizeGitRepo, runCli } from "../src/cli"; import { loadConfig, parseConfig } from "../src/config"; import type { CapletsError } from "../src/errors"; -import { writeTokenBundle } from "../src/auth"; +import { readTokenBundle, writeTokenBundle } from "../src/auth"; describe("cli init", () => { const originalMode = process.env.CAPLETS_MODE; @@ -2133,6 +2133,76 @@ describe("cli init", () => { } }); + it("refreshes configured OAuth credentials on demand", async () => { + const dir = mkdtempSync(join(tmpdir(), "caplets-auth-refresh-cli-")); + const authDir = join(dir, "auth"); + const configPath = join(dir, "config.json"); + const out: string[] = []; + const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue( + Response.json({ + access_token: "new-access-token", + refresh_token: "new-refresh-token", + token_type: "Bearer", + expires_in: 3600, + }), + ); + try { + writeFileSync( + configPath, + JSON.stringify({ + mcpServers: { + remote: { + name: "Remote", + description: "A useful remote OAuth server.", + transport: "http", + url: "https://example.com/mcp", + auth: { + type: "oauth2", + clientId: "client", + tokenUrl: "https://auth.example.com/token", + }, + }, + }, + }), + ); + process.env.CAPLETS_CONFIG = configPath; + writeTokenBundle( + { + server: "remote", + authType: "oauth2", + accessToken: "old-access-token", + refreshToken: "old-refresh-token", + expiresAt: "2999-01-01T00:00:00.000Z", + }, + authDir, + ); + + await runCli(["auth", "refresh", "remote"], { + writeOut: (value) => out.push(value), + authDir, + }); + + expect(out.join("")).toBe("Refreshed OAuth credentials for `remote`.\n"); + expect(fetchMock).toHaveBeenCalledWith( + "https://auth.example.com/token", + expect.objectContaining({ + method: "POST", + body: expect.stringContaining("grant_type=refresh_token"), + }), + ); + expect(String(fetchMock.mock.calls[0]?.[1]?.body)).toContain( + "refresh_token=old-refresh-token", + ); + expect(readTokenBundle("remote", authDir)).toMatchObject({ + accessToken: "new-access-token", + refreshToken: "new-refresh-token", + tokenType: "Bearer", + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("logs out configured OpenAPI OAuth endpoints", async () => { const dir = mkdtempSync(join(tmpdir(), "caplets-auth-")); const authDir = join(dir, "auth"); @@ -2342,6 +2412,33 @@ describe("cli setup", () => { ); }); + it("writes the versioned URL for remote generic MCP client setup", async () => { + const dir = mkdtempSync(join(tmpdir(), "caplets-setup-")); + const output = join(dir, "caplets.remote.json"); + + try { + await runCli( + [ + "setup", + "mcp-client", + "--remote-url", + "https://caplets.example.test/caplets", + "--output", + output, + ], + { writeOut: () => {} }, + ); + + expect(JSON.parse(readFileSync(output, "utf8"))).toEqual({ + mcpServers: { + caplets: { url: "https://caplets.example.test/caplets/v1/mcp" }, + }, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("runs remote OpenCode setup and reports JSON output", async () => { const out: string[] = []; const commands: Array<{ command: string; args: string[] }> = []; diff --git a/packages/core/test/code-mode-public-api.test.ts b/packages/core/test/code-mode-public-api.test.ts new file mode 100644 index 00000000..5106caef --- /dev/null +++ b/packages/core/test/code-mode-public-api.test.ts @@ -0,0 +1,113 @@ +import { existsSync, readFileSync, statSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import ts from "typescript"; +import { describe, expect, it } from "vitest"; +import { + codeModeDeclarationHash, + generateCodeModeDeclarations, + generateCodeModeRunToolDescription, + hasDirectFetchCall, + hasExecutableImport, + minifyCodeModeDeclarationText, +} from "../src/code-mode/index"; + +const codeModeSourceRoot = resolve(import.meta.dirname, "../src/code-mode"); + +describe("@caplets/core/code-mode public API", () => { + it("imports as a pure API entrypoint without runtime-only exports", () => { + const entrypoint = readFileSync(resolve(codeModeSourceRoot, "index.ts"), "utf8"); + + expect(entrypoint).not.toContain("codeModeRunInputSchema"); + expect(entrypoint).not.toContain("runCodeMode"); + expect(entrypoint).not.toContain("QuickJsCodeModeSandbox"); + expect(transitiveValueImports(resolve(codeModeSourceRoot, "index.ts"))).toEqual([ + "static-analysis.ts imports @babel/parser", + ]); + }); + + it("exports declaration helpers and static diagnostics through the public entrypoint", () => { + const declaration = generateCodeModeDeclarations({ + caplets: [{ id: "docs", name: "Docs", description: "Documentation search." }], + }); + + expect(declaration).toContain('docs:CapletHandle<"docs">'); + expect(codeModeDeclarationHash(declaration)).toMatch(/^[a-f0-9]{64}$/u); + expect(minifyCodeModeDeclarationText("type X = { y: string };")).toBe("type X={y:string};"); + expect(hasDirectFetchCall('const x = "fetch(\\"https://example.com\\")";')).toBe(false); + expect(hasDirectFetchCall('await fetch("https://example.com");')).toBe(true); + expect(hasExecutableImport('const x = "import fs from \\"node:fs\\"";')).toBe(false); + expect(hasExecutableImport('await import("node:fs");')).toBe(true); + expect(generateCodeModeRunToolDescription(declaration)).toContain( + "Generated declaration hints:", + ); + expect(generateCodeModeRunToolDescription(declaration)).toContain( + 'const h=caplets["caplet-id"]', + ); + }); +}); + +function transitiveValueImports(entrypoint: string): string[] { + const visited = new Set(); + const externalImports = new Set(); + const visit = (filePath: string): void => { + if (visited.has(filePath)) return; + visited.add(filePath); + + const source = readFileSync(filePath, "utf8"); + for (const specifier of valueSpecifiers(source)) { + if (!specifier.startsWith(".")) { + externalImports.add(`${formatCodeModePath(filePath)} imports ${specifier}`); + continue; + } + + const child = resolveModule(dirname(filePath), specifier); + if (child) visit(child); + } + }; + + visit(entrypoint); + return [...externalImports].sort(); +} + +function valueSpecifiers(source: string): string[] { + const specifiers: string[] = []; + + const sourceFile = ts.createSourceFile( + "/caplets-code-mode-public-api.ts", + source, + ts.ScriptTarget.ES2022, + true, + ts.ScriptKind.TS, + ); + + for (const statement of sourceFile.statements) { + if (ts.isImportDeclaration(statement)) { + if (!statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier)) { + specifiers.push(statement.moduleSpecifier.text); + } + continue; + } + + if (ts.isExportDeclaration(statement)) { + if ( + !statement.isTypeOnly && + statement.moduleSpecifier && + ts.isStringLiteral(statement.moduleSpecifier) + ) { + specifiers.push(statement.moduleSpecifier.text); + } + } + } + + return specifiers; +} + +function resolveModule(baseDir: string, specifier: string): string | undefined { + const exact = resolve(baseDir, specifier); + const candidates = [exact, `${exact}.ts`, resolve(exact, "index.ts")]; + return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile()); +} + +function formatCodeModePath(filePath: string): string { + return filePath.slice(codeModeSourceRoot.length + 1); +} diff --git a/packages/core/test/code-mode-static-analysis.test.ts b/packages/core/test/code-mode-static-analysis.test.ts index 3b78db8b..e1d833f8 100644 --- a/packages/core/test/code-mode-static-analysis.test.ts +++ b/packages/core/test/code-mode-static-analysis.test.ts @@ -4,10 +4,15 @@ import { hasDirectFetchCall, hasExecutableImport } from "../src/code-mode/static describe("Code Mode static analysis", () => { it("detects direct fetch calls without blocking text or member fetch calls", () => { expect(hasDirectFetchCall('await fetch("https://example.com");')).toBe(true); + expect(hasDirectFetchCall('return await fetch("https://example.com");')).toBe(true); expect(hasDirectFetchCall('await globalThis.fetch("https://example.com");')).toBe(true); + expect(hasDirectFetchCall('await globalThis["fetch"]("https://example.com");')).toBe(true); expect(hasDirectFetchCall('const note = "await fetch(\\"https://example.com\\")";')).toBe( false, ); + expect( + hasDirectFetchCall("const fetch = client.fetch; await globalThis[fetch]('/issues');"), + ).toBe(false); expect(hasDirectFetchCall("const result = client.fetch('/issues');")).toBe(false); }); @@ -16,8 +21,10 @@ describe("Code Mode static analysis", () => { expect(hasExecutableImport('import { readFile } from "node:fs";')).toBe(true); expect(hasExecutableImport('import "node:fs";')).toBe(true); expect(hasExecutableImport('await import("node:fs");')).toBe(true); + expect(hasExecutableImport('return await import("node:fs");')).toBe(true); expect(hasExecutableImport('export { readFile } from "node:fs";')).toBe(true); expect(hasExecutableImport('const note = "import fs from node:fs";')).toBe(false); + expect(hasExecutableImport("const value = 1; export { value };")).toBe(false); expect(hasExecutableImport("const result = client.import('value');")).toBe(false); }); }); diff --git a/packages/core/test/config.test.ts b/packages/core/test/config.test.ts index 1b528b11..46912b04 100644 --- a/packages/core/test/config.test.ts +++ b/packages/core/test/config.test.ts @@ -1850,8 +1850,7 @@ describe("config", () => { it("loads top-level Caplets options", () => { const config = parseConfig({ - $schema: - "https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplets-config.schema.json", + $schema: "https://caplets.dev/config.schema.json", defaultSearchLimit: 5, maxSearchLimit: 10, mcpServers: {}, diff --git a/packages/core/test/doctor-cli.test.ts b/packages/core/test/doctor-cli.test.ts index 390e5743..80f7bd56 100644 --- a/packages/core/test/doctor-cli.test.ts +++ b/packages/core/test/doctor-cli.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; +import { CloudAuthStore } from "../src/cloud-auth/store"; import { runCli } from "../src/cli"; +import { hostedCredentials, tempCloudAuthPath } from "./fixtures/cloud-auth"; describe("caplets doctor", () => { it("shows sectioned local diagnostics without stale presence wording", async () => { @@ -35,16 +37,48 @@ describe("caplets doctor", () => { }); const report = out.join(""); - expect(report).toContain("MCP URL: https://cloud.caplets.dev/ws/ian/mcp"); - expect(report).toContain("Control URL: https://cloud.caplets.dev/ws/ian/control"); - expect(report).toContain("Health URL: https://cloud.caplets.dev/ws/ian/healthz"); + expect(report).toContain("MCP URL: https://cloud.caplets.dev/v1/ws/ian/mcp"); + expect(report).toContain("Control URL: https://cloud.caplets.dev/v1/admin"); + expect(report).toContain("Health URL: https://cloud.caplets.dev/v1/healthz"); expect(report).toContain( - "WebSocket URL: wss://cloud.caplets.dev/ws/ian/control/project-bindings/connect", + "WebSocket URL: wss://cloud.caplets.dev/v1/ws/ian/attach/project-bindings/connect", ); expect(report).toContain("Auth: bearer"); expect(report).not.toContain("secret"); }); + it("uses saved Cloud Auth workspace for bare hosted Cloud remote URLs", async () => { + const path = tempCloudAuthPath(); + await new CloudAuthStore({ path }).save( + hostedCredentials({ + accessToken: "cloud-access", + workspaceSlug: "personal-c9b49d", + }), + ); + const out: string[] = []; + + await runCli(["doctor", "--json"], { + env: { + CAPLETS_MODE: "cloud", + CAPLETS_REMOTE_URL: "https://cloud.caplets.dev", + CAPLETS_CLOUD_AUTH_PATH: path, + }, + writeOut: (value) => out.push(value), + }); + + expect(JSON.parse(out.join(""))).toMatchObject({ + remote: { + configured: true, + mcpUrl: "https://cloud.caplets.dev/v1/ws/personal-c9b49d/mcp", + webSocketUrl: + "wss://cloud.caplets.dev/v1/ws/personal-c9b49d/attach/project-bindings/connect", + auth: "bearer", + tokenPresent: true, + workspace: "personal-c9b49d", + }, + }); + }); + it("emits JSON diagnostics with separate server, remote, binding, sync, daemon, and auth sections", async () => { const out: string[] = []; diff --git a/packages/core/test/exposure-direct-names.test.ts b/packages/core/test/exposure-direct-names.test.ts index 83492777..34607d33 100644 --- a/packages/core/test/exposure-direct-names.test.ts +++ b/packages/core/test/exposure-direct-names.test.ts @@ -3,6 +3,7 @@ import { decodeDirectResourceUri, directPromptName, directResourceTemplateUri, + directResourceUriMatchesTemplate, directResourceUri, directToolName, nativeDirectToolName, @@ -26,4 +27,51 @@ describe("direct exposure names", () => { "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2Fsrc%2F%7Bpath%7D", ); }); + + it("wraps malformed direct resource URI escapes as request errors", () => { + expect(() => decodeDirectResourceUri("caplets://docs/resources/%E0%A4%A")).toThrow( + expect.objectContaining({ code: "REQUEST_INVALID" }), + ); + }); + + it("matches optional URI template query expansions", () => { + expect( + directResourceUriMatchesTemplate( + "https://api.example.com/search", + "https://api.example.com/search{?q}", + ), + ).toBe(true); + expect( + directResourceUriMatchesTemplate( + "https://api.example.com/search?q=caplets", + "https://api.example.com/search{?q}", + ), + ).toBe(true); + }); + + it("does not let simple URI template variables cross path segments", () => { + expect( + directResourceUriMatchesTemplate( + "https://api.example.com/users/a/b", + "https://api.example.com/users/{id}", + ), + ).toBe(false); + expect( + directResourceUriMatchesTemplate( + "https://api.example.com/users/a/b", + "https://api.example.com/users/{+id}", + ), + ).toBe(true); + }); + + it("matches many optional URI template query variables without a variable cap", () => { + const variables = Array.from({ length: 32 }, (_value, index) => `v${index}`); + + expect( + directResourceUriMatchesTemplate( + "https://api.example.com/search?v0=first&v31=last", + `https://api.example.com/search{?${variables.join(",")}}`, + ), + ).toBe(true); + }); }); diff --git a/packages/core/test/native-options.test.ts b/packages/core/test/native-options.test.ts index 01a9b1c2..0fb84979 100644 --- a/packages/core/test/native-options.test.ts +++ b/packages/core/test/native-options.test.ts @@ -17,7 +17,7 @@ describe("resolveNativeCapletsServiceOptions", () => { ).toMatchObject({ mode: "remote", remote: { - url: new URL("http://127.0.0.1:5387/mcp"), + url: new URL("http://127.0.0.1:5387/v1/attach"), auth: { enabled: false, user: "caplets" }, pollIntervalMs: 30_000, }, @@ -30,16 +30,28 @@ describe("resolveNativeCapletsServiceOptions", () => { {}, { CAPLETS_REMOTE_URL: "https://cloud.caplets.dev", + CAPLETS_REMOTE_WORKSPACE: "personal", }, ), ).toMatchObject({ mode: "cloud", remote: { - url: new URL("https://cloud.caplets.dev/mcp"), + url: new URL("https://cloud.caplets.dev/v1/ws/personal/attach"), }, }); }); + it("rejects cloud mode without a workspace", () => { + expect(() => + resolveNativeCapletsServiceOptions( + {}, + { + CAPLETS_REMOTE_URL: "https://cloud.caplets.dev", + }, + ), + ).toThrow(/workspace/u); + }); + it("uses cloud mode when CAPLETS_MODE=cloud is explicit", () => { expect( resolveNativeCapletsServiceOptions( @@ -47,6 +59,7 @@ describe("resolveNativeCapletsServiceOptions", () => { { CAPLETS_MODE: "cloud", CAPLETS_REMOTE_URL: "https://cloud.caplets.dev", + CAPLETS_REMOTE_WORKSPACE: "personal", }, ), ).toMatchObject({ mode: "cloud" }); @@ -130,7 +143,7 @@ describe("resolveNativeCapletsServiceOptions", () => { ).toMatchObject({ mode: "remote", remote: { - url: new URL("https://configured.example.com/caplets/mcp"), + url: new URL("https://configured.example.com/caplets/v1/attach"), auth: { enabled: true, user: "configured", password: configPassword }, }, }); @@ -174,7 +187,7 @@ describe("resolveNativeCapletsServiceOptions", () => { ); expect(resolved.mode).toBe("remote"); expect(resolved.mode === "remote" ? resolved.remote.url : undefined).toEqual( - new URL("https://caplets.example.com/caplets/mcp"), + new URL("https://caplets.example.com/caplets/v1/attach"), ); expect(resolved.mode === "remote" ? resolved.remote.pollIntervalMs : undefined).toBe(5_000); expect(resolved.mode === "remote" ? resolved.remote.requestInit.headers : undefined).toEqual({ diff --git a/packages/core/test/native-remote.test.ts b/packages/core/test/native-remote.test.ts index 94d2df46..f19ad74c 100644 --- a/packages/core/test/native-remote.test.ts +++ b/packages/core/test/native-remote.test.ts @@ -5,17 +5,23 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { CapletsError } from "../src/errors"; import { CloudAuthStore } from "../src/cloud-auth/store"; -import { RemoteNativeCapletsService, type RemoteCapletsClient } from "../src/native/remote"; +import { CapletsEngine } from "../src/engine"; +import { + createSdkRemoteCapletsClient, + RemoteNativeCapletsService, + type RemoteCapletsClient, + type RemoteCapletsTool, +} from "../src/native/remote"; import { createNativeCapletsService, resetNativeProjectBindingFallbackWarningForTests, } from "../src/native/service"; +import { createHttpServeApp } from "../src/serve/http"; +import type { HttpServeOptions } from "../src/serve/options"; import { hostedCredentials, tempCloudAuthPath } from "./fixtures/cloud-auth"; function client( - tools: Array<{ name: string; title?: string | undefined; description?: string | undefined }> = [ - { name: "alpha", title: "Alpha", description: "Remote alpha" }, - ], + tools: RemoteCapletsTool[] = [{ name: "alpha", title: "Alpha", description: "Remote alpha" }], ) { const listeners = new Set<() => void>(); return { @@ -32,12 +38,137 @@ function client( for (const listener of listeners) listener(); }, listenerCount: () => listeners.size, - setTools: (next: typeof tools) => { + setTools: (next: RemoteCapletsTool[]) => { tools = next; }, }; } +function attachManifest(revision: string, exportId: string) { + return { + version: 1, + revision, + generatedAt: new Date(0).toISOString(), + caplets: [ + { + stableId: "progressive:remote", + exportId, + kind: "caplet", + name: "remote", + title: "Remote", + description: "Remote Caplet.", + inputSchema: { type: "object" }, + schemaHash: "sha256:same", + capletId: "remote", + shadowing: "forbid", + }, + ], + tools: [], + resources: [], + resourceTemplates: [], + prompts: [], + completions: [], + codeModeCaplets: [], + diagnostics: [], + }; +} + +function attachManifestWithDirectTool(revision: string, exportId: string) { + return { + ...attachManifest(revision, exportId), + caplets: [], + tools: [ + { + stableId: "tool:shared:ping", + exportId, + kind: "tool", + name: "shared__ping", + downstreamName: "ping", + title: "Ping", + description: "Ping direct tool.", + inputSchema: { + type: "object", + properties: { message: { type: "string" } }, + required: ["message"], + }, + outputSchema: { type: "object" }, + annotations: { readOnlyHint: true }, + schemaHash: "sha256:tool", + capletId: "shared", + shadowing: "forbid", + }, + ], + }; +} + +function attachManifestWithDirectMcpPrimitives(revision: string) { + return { + ...attachManifest(revision, "export-unused"), + caplets: [], + resources: [ + { + stableId: "resource:docs:file:///README.md", + exportId: "export-resource", + kind: "resource", + uri: "caplets://docs/resources/file%3A%2F%2F%2FREADME.md", + downstreamUri: "file:///README.md", + title: "README", + description: "README resource.", + mimeType: "text/markdown", + size: 42, + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + resourceTemplates: [ + { + stableId: "resourceTemplate:docs:file:///{path}", + exportId: "export-resource-template", + kind: "resourceTemplate", + uriTemplate: "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2F%7Bpath%7D", + downstreamUriTemplate: "file:///{path}", + title: "File", + description: "File resource.", + mimeType: "text/plain", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + prompts: [ + { + stableId: "prompt:docs:explain", + exportId: "export-prompt", + kind: "prompt", + name: "docs__explain", + downstreamName: "explain", + title: "Explain", + description: "Explain prompt.", + inputSchema: { + arguments: [{ name: "topic", description: "Topic to explain.", required: true }], + }, + schemaHash: "sha256:prompt", + capletId: "docs", + shadowing: "forbid", + }, + ], + completions: [ + { + stableId: "completion:docs", + exportId: "export-completion", + kind: "completion", + name: "docs:complete", + title: "Complete", + description: "Complete docs.", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + }; +} + function deferred() { let resolve!: (value: T | PromiseLike) => void; let reject!: (reason?: unknown) => void; @@ -49,6 +180,867 @@ function deferred() { } describe("RemoteNativeCapletsService", () => { + it("refetches and retries once when attach invoke reports a stale manifest", async () => { + const requests: Array<{ url: string; body?: unknown }> = []; + const fetchStub: typeof fetch = vi.fn(async (input, init) => { + const url = String(input); + requests.push({ + url, + ...(init?.body ? { body: JSON.parse(String(init.body)) } : {}), + }); + if (url.endsWith("/manifest")) { + const revision = requests.filter((request) => request.url.endsWith("/manifest")).length; + return Response.json(attachManifest(`rev-${revision}`, `export-${revision}`)); + } + if ( + url.endsWith("/invoke") && + requests.filter((request) => request.url.endsWith("/invoke")).length === 1 + ) { + return Response.json( + { ok: false, error: { code: "ATTACH_MANIFEST_STALE", message: "stale" } }, + { status: 409 }, + ); + } + return Response.json({ ok: true, data: { retried: true } }); + }); + const client = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await client.listTools(); + await expect(client.callTool("remote", { ok: true })).resolves.toEqual({ retried: true }); + + expect(requests.map((request) => request.url)).toEqual([ + "https://caplets.example.com/v1/attach/manifest", + "https://caplets.example.com/v1/attach/invoke", + "https://caplets.example.com/v1/attach/manifest", + "https://caplets.example.com/v1/attach/invoke", + ]); + expect(requests[1]?.body).toMatchObject({ revision: "rev-1", exportId: "export-1" }); + expect(requests[3]?.body).toMatchObject({ revision: "rev-2", exportId: "export-2" }); + + await client.close(); + }); + + it("throws a non-stale error when a refreshed manifest no longer has a compatible export", async () => { + const fetchStub: typeof fetch = vi.fn(async (input, _init) => { + const url = String(input); + if (url.endsWith("/manifest")) { + const manifestFetches = (fetchStub as ReturnType).mock.calls.filter( + ([call]) => String(call).endsWith("/manifest"), + ).length; + return Response.json( + manifestFetches === 1 + ? attachManifest("rev-1", "export-1") + : { ...attachManifest("rev-2", "export-2"), caplets: [] }, + ); + } + if (url.endsWith("/invoke")) { + return Response.json( + { ok: false, error: { code: "ATTACH_MANIFEST_STALE", message: "stale" } }, + { status: 409 }, + ); + } + return Response.json({ ok: true }); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect(remote.callTool("remote", {})).rejects.toMatchObject({ + code: "ATTACH_EXPORT_NOT_FOUND", + }); + await remote.close(); + }); + + it("keeps progressive caplet lookups from being overwritten by same-caplet direct exports", async () => { + const requests: Array<{ url: string; body?: unknown }> = []; + const fetchStub: typeof fetch = vi.fn(async (input, init) => { + const url = String(input); + requests.push({ + url, + ...(init?.body ? { body: JSON.parse(String(init.body)) } : {}), + }); + if (url.endsWith("/manifest")) { + return Response.json({ + ...attachManifest("rev-1", "export-caplet"), + tools: [ + { + stableId: "tool:remote:ping", + exportId: "export-tool", + kind: "tool", + name: "remote__ping", + downstreamName: "ping", + title: "Ping", + description: "Ping direct tool.", + inputSchema: { type: "object" }, + outputSchema: { type: "object" }, + schemaHash: "sha256:tool", + capletId: "remote", + shadowing: "forbid", + }, + ], + }); + } + return Response.json({ ok: true, data: { invoked: true } }); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect(remote.callTool("remote", { operation: "inspect" })).resolves.toEqual({ + invoked: true, + }); + + expect(requests.at(-1)?.body).toMatchObject({ + kind: "caplet", + exportId: "export-caplet", + input: { operation: "inspect" }, + }); + await remote.close(); + }); + + it("invokes code-mode-only caplets through exported attach IDs", async () => { + const requests: Array<{ url: string; body?: unknown }> = []; + const fetchStub: typeof fetch = vi.fn(async (input, init) => { + const url = String(input); + requests.push({ + url, + ...(init?.body ? { body: JSON.parse(String(init.body)) } : {}), + }); + if (url.endsWith("/manifest")) { + return Response.json({ + ...attachManifest("rev-1", "export-caplet"), + caplets: [], + tools: [], + codeModeCaplets: [ + { + stableId: "code_mode:remote", + exportId: "export-code-mode", + kind: "caplet", + name: "Remote", + title: "Remote", + description: "Remote Code Mode handle.", + inputSchema: { type: "object", additionalProperties: true }, + schemaHash: "hash-code-mode", + capletId: "remote", + shadowing: "forbid", + }, + ], + }); + } + return Response.json({ ok: true, data: { invoked: true } }); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect(remote.callTool("remote", { operation: "inspect" })).resolves.toEqual({ + invoked: true, + }); + + expect(requests.at(-1)?.body).toMatchObject({ + kind: "caplet", + exportId: "export-code-mode", + input: { operation: "inspect" }, + }); + await remote.close(); + }); + + it("notifies listeners when the attach events stream reports a manifest change", async () => { + let eventController: ReadableStreamDefaultController | undefined; + const encoder = new TextEncoder(); + const fetchStub: typeof fetch = vi.fn(async (input) => { + const url = String(input); + if (url.endsWith("/manifest")) return Response.json(attachManifest("rev-1", "export-1")); + if (url.endsWith("/events")) { + return new Response( + new ReadableStream({ + start(controller) { + eventController = controller; + }, + }), + { headers: { "content-type": "text/event-stream" } }, + ); + } + return Response.json({ ok: true }); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + const listener = vi.fn(); + + remote.onToolsChanged(listener); + await vi.waitFor(() => expect(eventController).toBeDefined()); + eventController!.enqueue( + encoder.encode('event: manifest_changed\ndata: {"revision":"rev-2"}\n\n'), + ); + + await vi.waitFor(() => expect(listener).toHaveBeenCalledOnce()); + await remote.close(); + }); + + it("reconnects the attach events stream after a server-side close", async () => { + vi.useFakeTimers(); + try { + const controllers: ReadableStreamDefaultController[] = []; + const fetchStub: typeof fetch = vi.fn(async (input) => { + const url = String(input); + if (url.endsWith("/manifest")) return Response.json(attachManifest("rev-1", "export-1")); + if (url.endsWith("/events")) { + return new Response( + new ReadableStream({ + start(controller) { + controllers.push(controller); + }, + }), + { headers: { "content-type": "text/event-stream" } }, + ); + } + return Response.json({ ok: true }); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + remote.onToolsChanged(vi.fn()); + await vi.waitFor(() => expect(controllers).toHaveLength(1)); + controllers[0]!.close(); + await vi.advanceTimersByTimeAsync(1_000); + + await vi.waitFor(() => expect(controllers).toHaveLength(2)); + await remote.close(); + } finally { + vi.useRealTimers(); + } + }); + + it("does not reconnect the attach events stream after an HTTP error response", async () => { + vi.useFakeTimers(); + try { + const fetchStub = vi.fn(async (input: Parameters[0]) => { + const url = String(input); + if (url.endsWith("/manifest")) return Response.json(attachManifest("rev-1", "export-1")); + if (url.endsWith("/events")) { + return Response.json({ ok: false, error: { code: "UNAUTHORIZED" } }, { status: 401 }); + } + return Response.json({ ok: true }); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub as typeof fetch, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + const eventRequestCount = () => + fetchStub.mock.calls.filter(([input]) => String(input).endsWith("/events")).length; + + remote.onToolsChanged(vi.fn()); + await vi.waitFor(() => expect(eventRequestCount()).toBe(1)); + await vi.advanceTimersByTimeAsync(5_000); + + expect(eventRequestCount()).toBe(1); + await remote.close(); + } finally { + vi.useRealTimers(); + } + }); + + it("does not reconnect the attach events stream after an empty success response", async () => { + vi.useFakeTimers(); + try { + const fetchStub = vi.fn(async (input: Parameters[0]) => { + const url = String(input); + if (url.endsWith("/manifest")) return Response.json(attachManifest("rev-1", "export-1")); + if (url.endsWith("/events")) { + return new Response(null, { status: 200 }); + } + return Response.json({ ok: true }); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub as typeof fetch, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + const eventRequestCount = () => + fetchStub.mock.calls.filter(([input]) => String(input).endsWith("/events")).length; + + remote.onToolsChanged(vi.fn()); + await vi.waitFor(() => expect(eventRequestCount()).toBe(1)); + await vi.advanceTimersByTimeAsync(5_000); + + expect(eventRequestCount()).toBe(1); + await remote.close(); + } finally { + vi.useRealTimers(); + } + }); + + it("does not retry non-attach errors that merely mention stale data", async () => { + const requests: Array<{ url: string; body?: unknown }> = []; + const fetchStub: typeof fetch = vi.fn(async (input, init) => { + const url = String(input); + requests.push({ + url, + ...(init?.body ? { body: JSON.parse(String(init.body)) } : {}), + }); + if (url.endsWith("/manifest")) { + return Response.json(attachManifest("rev-1", "export-1")); + } + return Response.json( + { ok: false, error: { code: "BACKEND_STALE_DATA", message: "data is stale" } }, + { status: 500 }, + ); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect(remote.callTool("remote", {})).rejects.toMatchObject({ + code: "BACKEND_STALE_DATA", + }); + + expect(requests.map((request) => request.url)).toEqual([ + "https://caplets.example.com/v1/attach/manifest", + "https://caplets.example.com/v1/attach/invoke", + ]); + await remote.close(); + }); + + it("preserves the source Caplet ID for remote direct-tool shadowing", async () => { + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input) => { + if (String(input).endsWith("/manifest")) { + return Response.json(attachManifestWithDirectTool("rev-1", "export-1")); + } + return Response.json({ ok: true, data: { pong: true } }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await expect(remote.listTools()).resolves.toEqual([ + expect.objectContaining({ + name: "shared__ping", + capletId: "shared", + }), + ]); + await remote.close(); + }); + + it("uses attached direct tool schemas instead of progressive operation args", async () => { + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input) => { + if (String(input).endsWith("/manifest")) { + return Response.json(attachManifestWithDirectTool("rev-1", "export-1")); + } + return Response.json({ ok: true, data: { pong: true } }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + const service = new RemoteNativeCapletsService({ client: remote, pollIntervalMs: 60_000 }); + + await service.reload(); + + expect(service.listTools()).toEqual([ + expect.objectContaining({ + caplet: "shared__ping", + inputSchema: { + type: "object", + properties: { message: { type: "string" } }, + required: ["message"], + }, + annotations: { readOnlyHint: true }, + }), + ]); + expect(service.listTools()[0]).not.toHaveProperty("operationNames"); + await service.close(); + }); + + it("surfaces direct MCP resources and prompts as native primitive tools", async () => { + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input) => { + if (String(input).endsWith("/manifest")) { + return Response.json(attachManifestWithDirectMcpPrimitives("rev-1")); + } + return Response.json({ ok: true, data: { invoked: true } }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + const service = new RemoteNativeCapletsService({ client: remote, pollIntervalMs: 60_000 }); + + await service.reload(); + + expect(configuredCapletIds(service.listTools())).toEqual( + expect.arrayContaining([ + "docs__list_resources", + "docs__read_resource", + "docs__list_resource_templates", + "docs__list_prompts", + "docs__get_prompt", + "docs__complete", + ]), + ); + await expect( + service.execute("docs__read_resource", { uri: "file:///README.md" }), + ).resolves.toEqual({ invoked: true }); + await service.close(); + }); + + it("passes prompt argument metadata through attached prompt lists", async () => { + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input) => { + if (String(input).endsWith("/manifest")) { + return Response.json(attachManifestWithDirectMcpPrimitives("rev-1")); + } + return Response.json({ ok: true, data: {} }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect(remote.callTool("docs__list_prompts", {})).resolves.toEqual({ + items: [ + { + name: "docs__explain", + title: "Explain", + description: "Explain prompt.", + arguments: [{ name: "topic", description: "Topic to explain.", required: true }], + }, + ], + }); + await remote.close(); + }); + + it("passes resource metadata through attached primitive lists", async () => { + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input) => { + if (String(input).endsWith("/manifest")) { + return Response.json(attachManifestWithDirectMcpPrimitives("rev-1")); + } + return Response.json({ ok: true, data: {} }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect(remote.callTool("docs__list_resources", {})).resolves.toEqual({ + items: [ + { + uri: "caplets://docs/resources/file%3A%2F%2F%2FREADME.md", + name: "README", + description: "README resource.", + mimeType: "text/markdown", + size: 42, + }, + ], + }); + await expect(remote.callTool("docs__list_resource_templates", {})).resolves.toEqual({ + items: [ + { + uriTemplate: "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2F%7Bpath%7D", + name: "File", + description: "File resource.", + mimeType: "text/plain", + }, + ], + }); + await remote.close(); + }); + + it("falls back to exact direct tool exports when names end with primitive suffixes", async () => { + const requests: Array<{ url: string; body?: unknown }> = []; + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input, init) => { + const url = String(input); + requests.push({ + url, + ...(init?.body ? { body: JSON.parse(String(init.body)) } : {}), + }); + if (url.endsWith("/manifest")) { + return Response.json({ + ...attachManifest("rev-1", "unused"), + caplets: [], + tools: [ + { + stableId: "tool:docs:read_resource", + exportId: "export-tool", + kind: "tool", + name: "docs__read_resource", + downstreamName: "read_resource", + title: "Read Resource Tool", + description: "A real downstream tool.", + inputSchema: { type: "object" }, + outputSchema: { type: "object" }, + schemaHash: "sha256:tool", + capletId: "docs", + shadowing: "forbid", + }, + ], + }); + } + return Response.json({ ok: true, data: { directTool: true } }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect( + remote.callTool("docs__read_resource", { uri: "file:///README.md" }), + ).resolves.toEqual({ directTool: true }); + + expect(requests.at(-1)?.body).toMatchObject({ + kind: "tool", + exportId: "export-tool", + input: { uri: "file:///README.md" }, + }); + await remote.close(); + }); + + it("does not overwrite real direct tools with generated primitive tools", async () => { + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input) => { + if (String(input).endsWith("/manifest")) { + return Response.json({ + ...attachManifestWithDirectMcpPrimitives("rev-1"), + tools: [ + { + stableId: "tool:docs:read_resource", + exportId: "export-tool", + kind: "tool", + name: "docs__read_resource", + downstreamName: "read_resource", + title: "Read Resource Tool", + description: "A real downstream tool.", + inputSchema: { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + }, + outputSchema: { type: "object" }, + schemaHash: "sha256:tool", + capletId: "docs", + shadowing: "forbid", + }, + ], + }); + } + return Response.json({ ok: true, data: { directTool: true } }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + const service = new RemoteNativeCapletsService({ client: remote, pollIntervalMs: 60_000 }); + + await service.reload(); + + const readResourceTools = service + .listTools() + .filter((tool) => tool.caplet === "docs__read_resource"); + expect(readResourceTools).toEqual([ + expect.objectContaining({ + inputSchema: { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + }, + }), + ]); + await service.close(); + }); + + it("invokes the direct resource template matching the requested resource URI", async () => { + const requests: Array<{ url: string; body?: unknown }> = []; + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input, init) => { + const url = String(input); + requests.push({ + url, + ...(init?.body ? { body: JSON.parse(String(init.body)) } : {}), + }); + if (url.endsWith("/manifest")) { + return Response.json({ + ...attachManifestWithDirectMcpPrimitives("rev-1"), + resources: [], + resourceTemplates: [ + { + stableId: "resourceTemplate:docs:file:///docs/{path}", + exportId: "export-doc-resource-template", + kind: "resourceTemplate", + uriTemplate: + "caplets://docs/resources/{encodedUri}?template=file%3A%2F%2F%2Fdocs%2F%7Bpath%7D", + downstreamUriTemplate: "file:///docs/{path}", + title: "Docs", + description: "Docs resource.", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + { + stableId: "resourceTemplate:docs:db:///{id}", + exportId: "export-db-resource-template", + kind: "resourceTemplate", + uriTemplate: + "caplets://docs/resources/{encodedUri}?template=db%3A%2F%2F%2F%7Bid%7D", + downstreamUriTemplate: "db:///{id}", + title: "Database", + description: "Database resource.", + schemaHash: null, + capletId: "docs", + shadowing: "forbid", + }, + ], + }); + } + return Response.json({ ok: true, data: { template: true } }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect(remote.callTool("docs__read_resource", { uri: "db:///123" })).resolves.toEqual({ + template: true, + }); + + expect(requests.at(-1)?.body).toMatchObject({ + kind: "resourceTemplate", + exportId: "export-db-resource-template", + input: { uri: "db:///123" }, + }); + await remote.close(); + }); + + it("retries stale manifests for primitive attached invokes", async () => { + const requests: Array<{ url: string; body?: unknown }> = []; + const fetchStub: typeof fetch = vi.fn(async (input, init) => { + const url = String(input); + requests.push({ + url, + ...(init?.body ? { body: JSON.parse(String(init.body)) } : {}), + }); + if (url.endsWith("/manifest")) { + const manifestFetches = requests.filter((request) => + request.url.endsWith("/manifest"), + ).length; + return Response.json( + manifestFetches === 1 + ? attachManifestWithDirectMcpPrimitives("rev-1") + : { + ...attachManifestWithDirectMcpPrimitives("rev-2"), + resources: [ + { + ...attachManifestWithDirectMcpPrimitives("rev-2").resources[0]!, + exportId: "export-resource-2", + }, + ], + }, + ); + } + if ( + url.endsWith("/invoke") && + requests.filter((request) => request.url.endsWith("/invoke")).length === 1 + ) { + return Response.json( + { ok: false, error: { code: "ATTACH_MANIFEST_STALE", message: "stale" } }, + { status: 409 }, + ); + } + return Response.json({ ok: true, data: { retried: true } }); + }); + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: fetchStub, + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + + await remote.listTools(); + await expect( + remote.callTool("docs__read_resource", { uri: "file:///README.md" }), + ).resolves.toEqual({ + retried: true, + }); + + expect(requests.map((request) => request.url)).toEqual([ + "https://caplets.example.com/v1/attach/manifest", + "https://caplets.example.com/v1/attach/invoke", + "https://caplets.example.com/v1/attach/manifest", + "https://caplets.example.com/v1/attach/invoke", + ]); + expect(requests.at(-1)?.body).toMatchObject({ + revision: "rev-2", + exportId: "export-resource-2", + }); + await remote.close(); + }); + + it("advertises the Code Mode run input schema for attached Code Mode", async () => { + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input) => { + if (String(input).endsWith("/manifest")) { + return Response.json({ + ...attachManifest("rev-1", "unused"), + caplets: [], + codeModeCaplets: [ + { + stableId: "code_mode:remote-only", + exportId: "export-remote-only", + kind: "caplet", + name: "Remote Only", + description: "Remote-only handle.", + schemaHash: null, + capletId: "remote-only", + shadowing: "forbid", + }, + ], + }); + } + return Response.json({ ok: true, data: {} }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + const service = new RemoteNativeCapletsService({ client: remote, pollIntervalMs: 60_000 }); + + await service.reload(); + + expect(service.listTools()).toContainEqual( + expect.objectContaining({ + caplet: "code_mode", + codeModeRun: true, + inputSchema: expect.objectContaining({ + required: ["code"], + properties: expect.objectContaining({ code: expect.any(Object) }), + }), + }), + ); + await service.close(); + }); + + it("preserves non-JSON attach invoke HTTP status for auth classification", async () => { + const remote = createSdkRemoteCapletsClient({ + url: new URL("https://caplets.example.com/v1/attach"), + requestInit: {}, + fetch: vi.fn(async (input) => { + if (String(input).endsWith("/manifest")) { + return Response.json(attachManifest("rev-1", "export-1")); + } + return new Response("gateway error", { status: 401 }); + }), + auth: { enabled: false, user: "caplets" }, + pollIntervalMs: 60_000, + }); + const service = new RemoteNativeCapletsService({ + client: remote, + pollIntervalMs: 60_000, + authKind: "hosted_cloud", + }); + + await service.reload(); + + await expect(service.execute("remote", {})).rejects.toMatchObject({ + code: "AUTH_FAILED", + message: "Caplets Cloud authentication failed; run caplets cloud auth login.", + }); + await service.close(); + }); + + it("runs advertised remote Code Mode handles from standalone remote services", async () => { + const fixture = client([ + { + name: "code_mode", + capletId: "code_mode", + title: "Code Mode", + codeModeRun: true, + codeModeCaplets: [ + { + stableId: "code_mode:remote-only", + exportId: "export-remote-only", + kind: "caplet", + name: "Remote Only", + description: "Remote-only handle.", + schemaHash: null, + capletId: "remote-only", + shadowing: "forbid", + }, + ], + }, + ]); + const service = new RemoteNativeCapletsService({ client: fixture.api, pollIntervalMs: 60_000 }); + await service.reload(); + + await expect( + service.execute("code_mode", { + code: "return { keys: Object.keys(caplets).sort() };", + }), + ).resolves.toMatchObject({ + ok: true, + value: { keys: ["debug", "remote-only"] }, + }); + expect(fixture.api.callTool).not.toHaveBeenCalled(); + await service.close(); + }); + it("maps remote MCP tools to native Caplet tools", async () => { const fixture = client([{ name: "git-hub", title: undefined, description: "GitHub tools" }]); const service = new RemoteNativeCapletsService({ client: fixture.api, pollIntervalMs: 60_000 }); @@ -115,6 +1107,30 @@ describe("RemoteNativeCapletsService", () => { await service.close(); }); + it("does not reconnect and retry application errors that mention invalid inputs", async () => { + const first = client(); + const second = client(); + first.api.callTool = vi.fn(async () => { + throw new Error("REQUEST_INVALID: invalid argument"); + }); + const factory = vi.fn(() => second.api); + const service = new RemoteNativeCapletsService({ + client: first.api, + clientFactory: factory, + pollIntervalMs: 60_000, + }); + + await expect(service.execute("alpha", { input: true })).rejects.toThrow( + /REQUEST_INVALID: invalid argument/u, + ); + + expect(first.api.callTool).toHaveBeenCalledTimes(1); + expect(factory).not.toHaveBeenCalled(); + expect(second.api.callTool).not.toHaveBeenCalled(); + + await service.close(); + }); + it("notifies listeners when remote tool list changes", async () => { const fixture = client([{ name: "alpha", description: "Alpha" }]); const service = new RemoteNativeCapletsService({ client: fixture.api, pollIntervalMs: 60_000 }); @@ -445,7 +1461,7 @@ describe("createNativeCapletsService remote mode", () => { await secondService.close(); }); - it("lists local overlay Caplets after remote tools and shadows matching remote Caplets", async () => { + it("keeps remote Caplets and suppresses matching local overlays by default", async () => { const fixture = client([ { name: "shared", title: "Remote Shared" }, { name: "remote-only", title: "Remote Only" }, @@ -457,21 +1473,166 @@ describe("createNativeCapletsService remote mode", () => { }, }); dirs.push(dir); + const writeErr = vi.fn(); const service = createNativeCapletsService({ mode: "remote", server: { url: "http://127.0.0.1:5387" }, remoteClientFactory: vi.fn(() => fixture.api), configPath, projectConfigPath, + writeErr, }); await service.reload(); expect(configuredCapletTitles(service.listTools())).toEqual([ + ["shared", "Remote Shared"], ["remote-only", "Remote Only"], - ["shared", "Local Shared"], ["local-only", "Local Only"], ]); + expect(writeErr).toHaveBeenCalledWith( + "Local Caplet 'shared' is suppressed because the remote attach manifest forbids shadowing that Caplet ID.\n", + ); + await service.close(); + }); + + it("suppresses local direct tools by source Caplet ID when remote forbids shadowing", async () => { + const fixture = client([{ name: "shared", title: "Remote Shared" }]); + const writeErr = vi.fn(); + const localService = { + listTools: vi.fn(() => [ + { + caplet: "shared__ping", + sourceCaplet: "shared", + toolName: "caplets__shared__ping", + title: "Ping", + description: "Local direct tool.", + promptGuidance: [], + }, + ]), + execute: vi.fn(async () => ({ local: true })), + reload: vi.fn(async () => true), + onToolsChanged: vi.fn(() => () => undefined), + close: vi.fn(async () => undefined), + }; + const service = createNativeCapletsService({ + mode: "remote", + server: { url: "http://127.0.0.1:5387" }, + remoteClientFactory: vi.fn(() => fixture.api), + localServiceFactory: vi.fn(() => localService), + writeErr, + }); + + await service.reload(); + + expect(configuredCapletIds(service.listTools())).toEqual(["shared"]); + expect(service.listTools().map((tool) => tool.caplet)).not.toContain("shared__ping"); + expect(writeErr).toHaveBeenCalledWith( + "Local Caplet 'shared' is suppressed because the remote attach manifest forbids shadowing that Caplet ID.\n", + ); + await service.close(); + }); + + it("warns when a local Code Mode-only Caplet is suppressed by remote Code Mode", async () => { + const fixture = client([ + { + name: "code_mode", + capletId: "code_mode", + title: "Code Mode", + codeModeRun: true, + codeModeCaplets: [ + { + stableId: "code_mode:shared", + exportId: "export-shared", + kind: "caplet", + name: "Remote Shared", + description: "Remote shared handle.", + schemaHash: null, + capletId: "shared", + shadowing: "forbid", + }, + ], + }, + ]); + const { dir, configPath, projectConfigPath } = tempConfig({ + options: { exposure: "code_mode" }, + mcpServers: { + shared: { + name: "Local Shared", + description: "Local shared handle.", + command: process.execPath, + }, + }, + }); + dirs.push(dir); + const writeErr = vi.fn(); + const service = createNativeCapletsService({ + mode: "remote", + server: { url: "http://127.0.0.1:5387" }, + remoteClientFactory: vi.fn(() => fixture.api), + configPath, + projectConfigPath, + writeErr, + }); + + await service.reload(); + + expect(writeErr).toHaveBeenCalledWith( + "Local Caplet 'shared' is suppressed because the remote attach manifest forbids shadowing that Caplet ID.\n", + ); + await service.close(); + }); + + it("keeps visible local Code Mode handles in composite Code Mode declarations", async () => { + const fixture = client([{ name: "remote-only", title: "Remote Only" }]); + const { dir, configPath, projectConfigPath } = tempConfig({ + mcpServers: { + local: { name: "Local", description: "Local Caplet.", command: process.execPath }, + }, + }); + dirs.push(dir); + const service = createNativeCapletsService({ + mode: "remote", + server: { url: "http://127.0.0.1:5387" }, + remoteClientFactory: vi.fn(() => fixture.api), + configPath, + projectConfigPath, + }); + + await service.reload(); + + const tools = service.listTools(); + expect(configuredCapletIds(tools)).toEqual(["remote-only", "local"]); + expect(tools.find((tool) => tool.caplet === "code_mode")).toEqual( + expect.objectContaining({ + codeModeRun: true, + codeModeCaplets: expect.arrayContaining([ + expect.objectContaining({ id: "remote-only" }), + expect.objectContaining({ id: "local" }), + ]), + }), + ); + await service.close(); + }); + + it("does not make attach-visible remote tools callable from Code Mode when the manifest is explicit empty", async () => { + const fixture = client([{ name: "remote-only", title: "Remote Only", codeModeCaplets: [] }]); + const { dir, configPath, projectConfigPath } = tempConfig({ + options: { exposure: "code_mode" }, + }); + dirs.push(dir); + const service = createNativeCapletsService({ + mode: "remote", + server: { url: "http://127.0.0.1:5387" }, + remoteClientFactory: vi.fn(() => fixture.api), + configPath, + projectConfigPath, + }); + + await service.reload(); + + expect(configuredCapletIds(service.listTools())).toEqual(["remote-only"]); + expect(service.listTools().some((tool) => tool.caplet === "code_mode")).toBe(false); await service.close(); }); @@ -586,6 +1747,60 @@ describe("createNativeCapletsService remote mode", () => { } }); + it("loads code-mode-default remote Caplets from the attach endpoint", async () => { + const remoteConfig = tempConfig({ + options: { exposure: "code_mode" }, + httpApis: { + "stealth-browser-use": { + name: "Stealth Browser Use", + description: "Remote browser tools.", + baseUrl: "http://127.0.0.1:1", + auth: { type: "none" }, + actions: { ping: { method: "GET", path: "/ping" } }, + }, + }, + }); + const localConfig = tempConfig({ options: { exposure: "code_mode" } }); + dirs.push(remoteConfig.dir, localConfig.dir); + const remoteEngine = new CapletsEngine({ + configPath: remoteConfig.configPath, + projectConfigPath: remoteConfig.projectConfigPath, + watch: false, + }); + const app = createHttpServeApp(httpOptions(), remoteEngine, { writeErr: () => {} }); + const fetchFromApp: typeof fetch = async (input, init) => { + const request = new Request(input, init); + const headers = new Headers(request.headers); + headers.set("host", new URL(request.url).host); + return app.fetch(new Request(request, { headers })); + }; + const service = createNativeCapletsService({ + mode: "remote", + server: { url: "http://127.0.0.1:5387", fetch: fetchFromApp }, + configPath: localConfig.configPath, + projectConfigPath: localConfig.projectConfigPath, + }); + + try { + await service.reload(); + + expect(configuredCapletIds(service.listTools())).toEqual([]); + await expect( + service.execute("code_mode", { + code: "return { keys: Object.keys(caplets).sort() };", + }), + ).resolves.toMatchObject({ + ok: true, + value: { + keys: ["debug", "stealth-browser-use"], + }, + }); + } finally { + await service.close(); + await remoteEngine.close(); + } + }); + it("emits one merged tools-changed event only when the merged set changes", async () => { const fixture = client([{ name: "alpha", title: "Alpha" }]); const { dir, configPath, projectConfigPath } = tempConfig({ @@ -797,7 +2012,7 @@ describe("createNativeCapletsService remote mode", () => { const service = createNativeCapletsService({ mode: "cloud", - server: { url: "https://cloud.caplets.dev" }, + server: { url: "https://cloud.caplets.dev/v1/ws/personal/mcp" }, remoteClientFactory: factory, configPath, projectConfigPath, @@ -807,7 +2022,7 @@ describe("createNativeCapletsService remote mode", () => { expect(service.listTools().map((tool) => tool.caplet)).toContain("remote"); expect(factory).toHaveBeenCalledWith( expect.objectContaining({ - url: new URL("https://cloud.caplets.dev/ws/personal/mcp"), + url: new URL("https://cloud.caplets.dev/v1/ws/personal/attach"), requestInit: { headers: { Authorization: "Bearer cloud-access" } }, }), ); @@ -1036,3 +2251,19 @@ function configuredCapletTitles(tools: Array<{ caplet: string; title: string }>) .filter((tool) => tool.caplet !== "code_mode") .map((tool) => [tool.caplet, tool.title]); } + +function httpOptions(overrides: Partial = {}): HttpServeOptions { + return { + transport: "http", + host: "127.0.0.1", + port: 5387, + path: "/", + publicOrigin: undefined, + auth: { enabled: false, user: "caplets" }, + allowUnauthenticatedHttp: false, + warnUnauthenticatedNetwork: false, + loopback: true, + trustProxy: false, + ...overrides, + }; +} diff --git a/packages/core/test/project-binding-integration.test.ts b/packages/core/test/project-binding-integration.test.ts index 6b4ae42e..9e203a75 100644 --- a/packages/core/test/project-binding-integration.test.ts +++ b/packages/core/test/project-binding-integration.test.ts @@ -62,7 +62,7 @@ describe("Project Binding integration", () => { expect(readFileSync(join(workspace.project, "build.js"), "utf8")).toContain("process.cwd()"); }); - it("preserves local overlay shadowing while remote-only Caplets execute remotely", async () => { + it("suppresses local overlay duplicates while remote-only Caplets execute remotely", async () => { const dir = mkdtempSync(join(tmpdir(), "caplets-project-binding-native-")); dirs.push(dir); const userDir = join(dir, "user"); @@ -96,18 +96,19 @@ describe("Project Binding integration", () => { await service.reload(); expect(configuredCapletTitles(service.listTools())).toEqual([ + ["build", "Remote Build"], ["deploy", "Remote Deploy"], - ["build", "Local Build"], ]); - await expect(service.execute("build", { operation: "inspect" })).resolves.toMatchObject({ - content: expect.any(Array), + await expect(service.execute("build", { input: true })).resolves.toEqual({ + name: "build", + args: { input: true }, }); await expect(service.execute("deploy", { input: true })).resolves.toEqual({ name: "deploy", args: { input: true }, }); - expect(remoteClient.callTool).toHaveBeenCalledTimes(1); + expect(remoteClient.callTool).toHaveBeenCalledTimes(2); await service.close(); }); }); diff --git a/packages/core/test/project-binding-routes.test.ts b/packages/core/test/project-binding-routes.test.ts index 441265e2..63a75da9 100644 --- a/packages/core/test/project-binding-routes.test.ts +++ b/packages/core/test/project-binding-routes.test.ts @@ -9,13 +9,13 @@ import { describe("project binding routes", () => { it("derives the connect URL from a hosted base URL", () => { expect(projectBindingConnectUrl("https://example.com/caplets")).toBe( - "https://example.com/caplets/control/project-bindings/connect", + "https://example.com/caplets/v1/attach/project-bindings/connect", ); }); it("derives a binding status URL from a hosted base URL", () => { expect(projectBindingStatusUrl("https://example.com/caplets", "bind_123")).toBe( - "https://example.com/caplets/control/project-bindings/bind_123/status", + "https://example.com/caplets/v1/attach/project-bindings/bind_123/status", ); }); diff --git a/packages/core/test/project-binding-session.test.ts b/packages/core/test/project-binding-session.test.ts index 1d8dc01d..834e8354 100644 --- a/packages/core/test/project-binding-session.test.ts +++ b/packages/core/test/project-binding-session.test.ts @@ -29,7 +29,7 @@ describe("runProjectBindingSession", () => { fetch: async (url, init) => { const body = init?.body ? JSON.parse(String(init.body)) : undefined; requests.push({ method: init?.method ?? "GET", url: String(url), body }); - if (String(url).endsWith("/control/project-bindings/sessions")) { + if (String(url).endsWith("/v1/attach/project-bindings/sessions")) { return Response.json( { binding: { bindingId: "binding_1", state: "attaching", syncState: "pending" }, diff --git a/packages/core/test/remote-control-client.test.ts b/packages/core/test/remote-control-client.test.ts index 99255525..649e3271 100644 --- a/packages/core/test/remote-control-client.test.ts +++ b/packages/core/test/remote-control-client.test.ts @@ -25,7 +25,7 @@ describe("RemoteControlClient", () => { }); expect(requests).toHaveLength(1); - expect(String(requests[0]?.input)).toBe("https://example.com/caplets/control"); + expect(String(requests[0]?.input)).toBe("https://example.com/caplets/v1/admin"); expect(requests[0]?.init).toMatchObject({ method: "POST", body: JSON.stringify({ command: "list", arguments: { verbose: true } }), diff --git a/packages/core/test/remote-control-dispatch.test.ts b/packages/core/test/remote-control-dispatch.test.ts index 5b8a2a71..57152373 100644 --- a/packages/core/test/remote-control-dispatch.test.ts +++ b/packages/core/test/remote-control-dispatch.test.ts @@ -10,13 +10,14 @@ vi.mock("@modelcontextprotocol/sdk/client/auth", async (importOriginal) => ({ auth: mockMcpAuth, })); -import { writeTokenBundle } from "../src/auth"; +import { readTokenBundle, writeTokenBundle } from "../src/auth"; import { RemoteAuthFlowStore } from "../src/remote-control/auth-flow"; import { dispatchRemoteCliRequest } from "../src/remote-control/dispatch"; const dirs: string[] = []; afterEach(() => { + vi.restoreAllMocks(); mockMcpAuth.mockReset(); for (const dir of dirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); @@ -368,12 +369,21 @@ describe("dispatchRemoteCliRequest", () => { expect(response.ok && response.result).toEqual(["server_status.check"]); }); - it("lists and logs out server-side auth credentials", async () => { + it("lists, refreshes, and logs out server-side auth credentials", async () => { const fixture = remoteFixtureWithOAuth(); + const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue( + Response.json({ + access_token: "new-access-token", + refresh_token: "new-refresh-token", + token_type: "Bearer", + expires_in: 3600, + }), + ); writeTokenBundle( { server: "remote", accessToken: "secret-access-token", + refreshToken: "old-refresh-token", expiresAt: "2999-01-01T00:00:00.000Z", }, fixture.context.authDir, @@ -388,6 +398,26 @@ describe("dispatchRemoteCliRequest", () => { result: [expect.objectContaining({ server: "remote", status: "authenticated" })], }); + const refreshed = await dispatchRemoteCliRequest( + { command: "auth_refresh", arguments: { server: "remote" } }, + fixture.context, + ); + expect(refreshed).toEqual({ + ok: true, + result: { server: "remote" }, + }); + expect(fetchMock).toHaveBeenCalledWith( + "https://auth.example.com/token", + expect.objectContaining({ + method: "POST", + body: expect.stringContaining("refresh_token=old-refresh-token"), + }), + ); + expect(readTokenBundle("remote", fixture.context.authDir)).toMatchObject({ + accessToken: "new-access-token", + refreshToken: "new-refresh-token", + }); + const loggedOut = await dispatchRemoteCliRequest( { command: "auth_logout", arguments: { server: "remote" } }, fixture.context, @@ -515,7 +545,11 @@ function remoteFixtureWithOAuth() { description: "Remote OAuth server.", transport: "http", url: "https://example.com/mcp", - auth: { type: "oauth2", clientId: "client" }, + auth: { + type: "oauth2", + clientId: "client", + tokenUrl: "https://auth.example.com/token", + }, }, }, }), diff --git a/packages/core/test/remote-options.test.ts b/packages/core/test/remote-options.test.ts index e068bb7b..9389dbc3 100644 --- a/packages/core/test/remote-options.test.ts +++ b/packages/core/test/remote-options.test.ts @@ -4,6 +4,7 @@ import type { CapletsError } from "../src/errors"; import { hostedCloudWorkspaceFromRemoteUrl, resolveCapletsRemote, + resolveHostedCloudRemote, resolveRemoteMode, } from "../src/remote/options"; @@ -93,11 +94,12 @@ describe("resolveCapletsRemote", () => { expect(resolved).toMatchObject({ baseUrl: new URL("https://example.com/caplets"), - mcpUrl: new URL("https://example.com/caplets/mcp"), - controlUrl: new URL("https://example.com/caplets/control"), - healthUrl: new URL("https://example.com/caplets/healthz"), + mcpUrl: new URL("https://example.com/caplets/v1/mcp"), + attachUrl: new URL("https://example.com/caplets/v1/attach"), + controlUrl: new URL("https://example.com/caplets/v1/admin"), + healthUrl: new URL("https://example.com/caplets/v1/healthz"), projectBindingWebSocketUrl: new URL( - "wss://example.com/caplets/control/project-bindings/connect", + "wss://example.com/caplets/v1/attach/project-bindings/connect", ), auth: { type: "basic", user: "env-user", password }, }); @@ -107,6 +109,12 @@ describe("resolveCapletsRemote", () => { ); }); + it("derives attach URL from self-hosted remote base paths", () => { + expect( + resolveCapletsRemote({}, { CAPLETS_REMOTE_URL: "https://host.example/base" }).attachUrl, + ).toEqual(new URL("https://host.example/base/v1/attach")); + }); + it("supports bearer token and workspace settings", () => { const resolved = resolveCapletsRemote( { token: "input-token", workspace: "team" }, @@ -133,7 +141,7 @@ describe("resolveCapletsRemote", () => { describe("hostedCloudWorkspaceFromRemoteUrl", () => { it("extracts workspace slugs from copied Cloud MCP endpoints", () => { expect( - hostedCloudWorkspaceFromRemoteUrl("https://cloud.caplets.dev/ws/personal-c9b49d/mcp"), + hostedCloudWorkspaceFromRemoteUrl("https://cloud.caplets.dev/v1/ws/personal-c9b49d/mcp"), ).toBe("personal-c9b49d"); }); @@ -143,3 +151,27 @@ describe("hostedCloudWorkspaceFromRemoteUrl", () => { ).toBeUndefined(); }); }); + +describe("resolveHostedCloudRemote", () => { + it("derives attach URL under the selected Cloud workspace path", () => { + const resolved = resolveHostedCloudRemote( + { workspace: "team-one" }, + { CAPLETS_REMOTE_URL: "https://cloud.caplets.dev" }, + ); + + expect(resolved.attachUrl).toEqual(new URL("https://cloud.caplets.dev/v1/ws/team-one/attach")); + }); + + it("uses CAPLETS_REMOTE_WORKSPACE when a Cloud URL has no workspace path", () => { + const resolved = resolveHostedCloudRemote( + {}, + { + CAPLETS_REMOTE_URL: "https://cloud.caplets.dev", + CAPLETS_REMOTE_WORKSPACE: "team-env", + }, + ); + + expect(resolved.workspace).toBe("team-env"); + expect(resolved.attachUrl).toEqual(new URL("https://cloud.caplets.dev/v1/ws/team-env/attach")); + }); +}); diff --git a/packages/core/test/remote-selection.test.ts b/packages/core/test/remote-selection.test.ts index 44deb370..46239ea8 100644 --- a/packages/core/test/remote-selection.test.ts +++ b/packages/core/test/remote-selection.test.ts @@ -80,11 +80,12 @@ describe("resolveRemoteSelection", () => { selectedWorkspace: "personal-c9b49d", remote: { baseUrl: new URL("https://cloud.pr-2.preview.caplets.dev/"), - mcpUrl: new URL("https://cloud.pr-2.preview.caplets.dev/ws/personal-c9b49d/mcp"), - controlUrl: new URL("https://cloud.pr-2.preview.caplets.dev/control"), - healthUrl: new URL("https://cloud.pr-2.preview.caplets.dev/healthz"), + mcpUrl: new URL("https://cloud.pr-2.preview.caplets.dev/v1/ws/personal-c9b49d/mcp"), + attachUrl: new URL("https://cloud.pr-2.preview.caplets.dev/v1/ws/personal-c9b49d/attach"), + controlUrl: new URL("https://cloud.pr-2.preview.caplets.dev/v1/admin"), + healthUrl: new URL("https://cloud.pr-2.preview.caplets.dev/v1/healthz"), projectBindingWebSocketUrl: new URL( - "wss://cloud.pr-2.preview.caplets.dev/control/project-bindings/connect", + "wss://cloud.pr-2.preview.caplets.dev/v1/ws/personal-c9b49d/attach/project-bindings/connect", ), }, }); @@ -114,9 +115,10 @@ describe("resolveRemoteSelection", () => { selectedWorkspace: "personal-c9b49d", remote: { baseUrl: new URL("https://cloud.pr-2.preview.caplets.dev/"), - mcpUrl: new URL("https://cloud.pr-2.preview.caplets.dev/ws/personal-c9b49d/mcp"), + mcpUrl: new URL("https://cloud.pr-2.preview.caplets.dev/v1/ws/personal-c9b49d/mcp"), + attachUrl: new URL("https://cloud.pr-2.preview.caplets.dev/v1/ws/personal-c9b49d/attach"), projectBindingWebSocketUrl: new URL( - "wss://cloud.pr-2.preview.caplets.dev/control/project-bindings/connect", + "wss://cloud.pr-2.preview.caplets.dev/v1/ws/personal-c9b49d/attach/project-bindings/connect", ), }, cloudPresence: { diff --git a/packages/core/test/serve-http.test.ts b/packages/core/test/serve-http.test.ts index ba9e9e43..93f5995e 100644 --- a/packages/core/test/serve-http.test.ts +++ b/packages/core/test/serve-http.test.ts @@ -26,21 +26,41 @@ describe("createHttpServeApp", () => { name: "caplets", transport: "http", base: "/", - mcp: "/mcp", - control: "/control", - health: "/healthz", + versions: [ + { + version: 1, + path: "/v1", + links: { + mcp: "/v1/mcp", + admin: "/v1/admin", + attachManifest: "/v1/attach/manifest", + attachEvents: "/v1/attach/events", + attachInvoke: "/v1/attach/invoke", + health: "/v1/healthz", + }, + }, + ], auth: { type: "basic", enabled: false }, }); - const health = await app.request("http://127.0.0.1:5387/healthz"); + const v1 = await app.request("http://127.0.0.1:5387/v1"); + expect(v1.status).toBe(200); + await expect(v1.json()).resolves.toMatchObject({ + version: 1, + links: { + mcp: "/v1/mcp", + admin: "/v1/admin", + attachManifest: "/v1/attach/manifest", + attachEvents: "/v1/attach/events", + attachInvoke: "/v1/attach/invoke", + health: "/v1/healthz", + }, + }); + + const health = await app.request("http://127.0.0.1:5387/v1/healthz"); expect(health.status).toBe(200); await expect(health.json()).resolves.toEqual({ status: "ok", - transport: "http", - base: "/", - mcpPath: "/mcp", - controlPath: "/control", - healthPath: "/healthz", }); await engine.close(); @@ -53,12 +73,12 @@ describe("createHttpServeApp", () => { writeErr: (value) => logs.push(value), }); - const response = await app.request("http://127.0.0.1:5387/healthz"); + const response = await app.request("http://127.0.0.1:5387/v1/healthz"); expect(response.status).toBe(200); - expect(logs.join("")).toContain("<-- GET /healthz"); + expect(logs.join("")).toContain("<-- GET /v1/healthz"); const plainLogs = logs.join("").replaceAll(String.fromCharCode(27), ""); - expect(plainLogs).toContain("--> GET /healthz"); + expect(plainLogs).toContain("--> GET /v1/healthz"); expect(plainLogs).toContain("200"); await engine.close(); @@ -73,11 +93,11 @@ describe("createHttpServeApp", () => { { writeErr: () => {} }, ); - const missing = await app.request("http://127.0.0.1:5387/mcp", { method: "POST" }); + const missing = await app.request("http://127.0.0.1:5387/v1/mcp", { method: "POST" }); expect(missing.status).toBe(401); expect(missing.headers.get("www-authenticate")).toContain("Basic"); - const wrong = await app.request("http://127.0.0.1:5387/mcp", { + const wrong = await app.request("http://127.0.0.1:5387/v1/mcp", { method: "POST", headers: { authorization: `Basic ${Buffer.from(`caplets:not-the-${testPassword}`).toString("base64")}`, @@ -88,6 +108,22 @@ describe("createHttpServeApp", () => { await engine.close(); }); + it("requires Basic Auth on attach manifest when password is configured", async () => { + const { engine } = testEngine(); + const testPassword = ["test", "password"].join("-"); + const app = createHttpServeApp( + httpOptions({ auth: { enabled: true, user: "caplets", password: testPassword } }), + engine, + { writeErr: () => {} }, + ); + + const missing = await app.request("http://127.0.0.1:5387/v1/attach/manifest"); + expect(missing.status).toBe(401); + expect(missing.headers.get("www-authenticate")).toContain("Basic"); + + await engine.close(); + }); + it("requires Basic Auth on control path and dispatches authenticated list requests", async () => { const context = testContext(); const engine = new CapletsEngine({ @@ -102,11 +138,11 @@ describe("createHttpServeApp", () => { { writeErr: () => {}, control: context }, ); - const missing = await app.request("http://127.0.0.1:5387/control", { method: "POST" }); + const missing = await app.request("http://127.0.0.1:5387/v1/admin", { method: "POST" }); expect(missing.status).toBe(401); expect(missing.headers.get("www-authenticate")).toContain("Basic"); - const listed = await app.request("http://127.0.0.1:5387/control", { + const listed = await app.request("http://127.0.0.1:5387/v1/admin", { method: "POST", headers: { authorization: `Basic ${Buffer.from(`caplets:${testPassword}`).toString("base64")}`, @@ -120,7 +156,7 @@ describe("createHttpServeApp", () => { await engine.close(); }); - it("exposes authenticated Project Binding status under the control namespace", async () => { + it("exposes authenticated Project Binding status under the attach namespace", async () => { const { engine } = testEngine(); const testPassword = ["test", "password"].join("-"); const app = createHttpServeApp( @@ -130,12 +166,12 @@ describe("createHttpServeApp", () => { ); const missing = await app.request( - "http://127.0.0.1:5387/control/project-bindings/bind_123/status", + "http://127.0.0.1:5387/v1/attach/project-bindings/bind_123/status", ); expect(missing.status).toBe(401); const response = await app.request( - "http://127.0.0.1:5387/control/project-bindings/bind_123/status", + "http://127.0.0.1:5387/v1/attach/project-bindings/bind_123/status", { headers: { authorization: `Basic ${Buffer.from(`caplets:${testPassword}`).toString("base64")}`, @@ -158,7 +194,7 @@ describe("createHttpServeApp", () => { }); const response = await app.request( - "http://127.0.0.1:5387/caplets/control/project-bindings/connect", + "http://127.0.0.1:5387/caplets/v1/attach/project-bindings/connect", ); expect(response.status).toBe(426); @@ -169,6 +205,40 @@ describe("createHttpServeApp", () => { await engine.close(); }); + it("mounts Project Binding session routes under the attach namespace", async () => { + const { engine } = testEngine(); + const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); + + const session = await app.request("http://127.0.0.1:5387/v1/attach/project-bindings/sessions", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ projectRoot: "/repo" }), + }); + expect(session.status).toBe(201); + const created = (await session.json()) as { + binding: { bindingId: string }; + sessionId: string; + }; + expect(created.binding.bindingId).toEqual(expect.any(String)); + expect(created.sessionId).toEqual(expect.any(String)); + + const heartbeat = await app.request( + `http://127.0.0.1:5387/v1/attach/project-bindings/${created.binding.bindingId}/heartbeat`, + { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }, + ); + expect(heartbeat.status).toBe(200); + await expect(heartbeat.json()).resolves.toMatchObject({ ok: true }); + + const ended = await app.request( + `http://127.0.0.1:5387/v1/attach/project-bindings/${created.binding.bindingId}/session`, + { method: "DELETE", headers: { "content-type": "application/json" }, body: "{}" }, + ); + expect(ended.status).toBe(200); + await expect(ended.json()).resolves.toMatchObject({ ok: true }); + + await engine.close(); + }); + it("mounts service routes under a base path", async () => { const { engine } = testEngine(); const app = createHttpServeApp(httpOptions({ path: "/caplets" }), engine, { @@ -178,14 +248,9 @@ describe("createHttpServeApp", () => { const rootHealth = await app.request("http://127.0.0.1:5387/healthz"); expect(rootHealth.status).toBe(404); - const health = await app.request("http://127.0.0.1:5387/caplets/healthz"); + const health = await app.request("http://127.0.0.1:5387/caplets/v1/healthz"); expect(health.status).toBe(200); - await expect(health.json()).resolves.toMatchObject({ - base: "/caplets", - mcpPath: "/caplets/mcp", - controlPath: "/caplets/control", - healthPath: "/caplets/healthz", - }); + await expect(health.json()).resolves.toEqual({ status: "ok" }); await engine.close(); }); @@ -210,12 +275,12 @@ describe("createHttpServeApp", () => { const rootControl = await app.request("http://127.0.0.1:5387/control", { method: "POST" }); expect(rootControl.status).toBe(404); - const missing = await app.request("http://127.0.0.1:5387/caplets/control", { + const missing = await app.request("http://127.0.0.1:5387/caplets/v1/admin", { method: "POST", }); expect(missing.status).toBe(401); - const listed = await app.request("http://127.0.0.1:5387/caplets/control", { + const listed = await app.request("http://127.0.0.1:5387/caplets/v1/admin", { method: "POST", headers: { authorization: `Basic ${Buffer.from(`caplets:${testPassword}`).toString("base64")}`, @@ -238,7 +303,7 @@ describe("createHttpServeApp", () => { }); const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {}, control: context }); - const response = await app.request("http://127.0.0.1:5387/control", { + const response = await app.request("http://127.0.0.1:5387/v1/admin", { method: "POST", headers: { "content-type": "application/json" }, body: "not-json", @@ -276,7 +341,7 @@ describe("createHttpServeApp", () => { }); const response = await app.request( - "http://127.0.0.1:5387/caplets/control/auth/callback/flow-1?code=abc&state=xyz", + "http://127.0.0.1:5387/caplets/v1/admin/auth/callback/flow-1?code=abc&state=xyz", ); expect(response.status).toBe(200); @@ -311,7 +376,7 @@ describe("createHttpServeApp", () => { }); const response = await app.request( - "http://127.0.0.1:5387/caplets/control/auth/callback/flow-1?code=abc&state=xyz", + "http://127.0.0.1:5387/caplets/v1/admin/auth/callback/flow-1?code=abc&state=xyz", ); expect(response.status).toBe(400); @@ -335,7 +400,7 @@ describe("createHttpServeApp", () => { control: context, }); - const response = await app.request("http://127.0.0.1:5387/control-api/control", { + const response = await app.request("http://127.0.0.1:5387/control-api/v1/admin", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ command: "auth_login_start", arguments: { server: "remote" } }), @@ -347,7 +412,7 @@ describe("createHttpServeApp", () => { const result = (body as { result: { authorizationUrl: string } }).result; const authorizationUrl = new URL(result.authorizationUrl); expect(authorizationUrl.searchParams.get("redirect_uri")).toMatch( - /^http:\/\/127\.0\.0\.1:5387\/control-api\/control\/auth\/callback\//u, + /^http:\/\/127\.0\.0\.1:5387\/control-api\/v1\/admin\/auth\/callback\//u, ); await engine.close(); @@ -369,7 +434,7 @@ describe("createHttpServeApp", () => { }, ); - const response = await app.request("http://127.0.0.1:5387/caplets/control", { + const response = await app.request("http://127.0.0.1:5387/caplets/v1/admin", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ command: "auth_login_start", arguments: { server: "remote" } }), @@ -381,7 +446,7 @@ describe("createHttpServeApp", () => { const result = (body as { result: { authorizationUrl: string } }).result; const authorizationUrl = new URL(result.authorizationUrl); expect(authorizationUrl.searchParams.get("redirect_uri")).toMatch( - /^https:\/\/caplets\.example\.com\/caplets\/control\/auth\/callback\//u, + /^https:\/\/caplets\.example\.com\/caplets\/v1\/admin\/auth\/callback\//u, ); await engine.close(); @@ -399,7 +464,7 @@ describe("createHttpServeApp", () => { control: context, }); - const response = await app.request("http://10.0.0.5:5387/caplets/control", { + const response = await app.request("http://10.0.0.5:5387/caplets/v1/admin", { method: "POST", headers: { "content-type": "application/json", @@ -415,7 +480,7 @@ describe("createHttpServeApp", () => { const result = (body as { result: { authorizationUrl: string } }).result; const authorizationUrl = new URL(result.authorizationUrl); expect(authorizationUrl.searchParams.get("redirect_uri")).toMatch( - /^http:\/\/10\.0\.0\.5:5387\/caplets\/control\/auth\/callback\//u, + /^http:\/\/10\.0\.0\.5:5387\/caplets\/v1\/admin\/auth\/callback\//u, ); await engine.close(); @@ -433,7 +498,7 @@ describe("createHttpServeApp", () => { control: context, }); - const response = await app.request("http://10.0.0.5:5387/caplets/control", { + const response = await app.request("http://10.0.0.5:5387/caplets/v1/admin", { method: "POST", headers: { "content-type": "application/json", @@ -449,7 +514,7 @@ describe("createHttpServeApp", () => { const result = (body as { result: { authorizationUrl: string } }).result; const authorizationUrl = new URL(result.authorizationUrl); expect(authorizationUrl.searchParams.get("redirect_uri")).toMatch( - /^https:\/\/caplets\.example\.com\/caplets\/control\/auth\/callback\//u, + /^https:\/\/caplets\.example\.com\/caplets\/v1\/admin\/auth\/callback\//u, ); await engine.close(); @@ -459,7 +524,7 @@ describe("createHttpServeApp", () => { const { engine } = testEngine(); const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); - const response = await app.request("http://127.0.0.1:5387/mcp/extra"); + const response = await app.request("http://127.0.0.1:5387/v1/mcp/extra"); expect(response.status).toBe(404); await engine.close(); @@ -469,66 +534,269 @@ describe("createHttpServeApp", () => { const { engine } = testEngine(); const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); - const init = await app.request("http://127.0.0.1:5387/mcp", { + const tools = await listMcpTools(app, "/v1/mcp"); + + expect(tools.map((tool) => tool.name)).toEqual(["code_mode"]); + + await engine.close(); + }); + + it("returns an attach manifest instead of serving MCP on attach", async () => { + const { engine } = testEngine(); + const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); + + const manifestResponse = await app.request("http://127.0.0.1:5387/v1/attach/manifest"); + expect(manifestResponse.status).toBe(200); + const manifest = await manifestResponse.json(); + + expect(manifest).toMatchObject({ + version: 1, + revision: expect.any(String), + generatedAt: expect.any(String), + caplets: [], + tools: [], + resources: [], + resourceTemplates: [], + prompts: [], + completions: [], + codeModeCaplets: [ + expect.objectContaining({ + stableId: "code_mode:status", + kind: "caplet", + capletId: "status", + name: "Status", + shadowing: "forbid", + }), + ], + diagnostics: [], + }); + + await engine.close(); + }); + + it("does not advertise attach routes when an HTTP app bridges an attached session", async () => { + const { engine } = testEngine(); + const app = createHttpServeApp(httpOptions(), engine, { + writeErr: () => {}, + exposeAttach: false, + sessionFactory: () => ({ + connect: async () => undefined, + close: async () => undefined, + }), + }); + + const discovery = (await (await app.request("http://127.0.0.1:5387/v1")).json()) as { + links: Record; + }; + expect(discovery.links).not.toHaveProperty("attachManifest"); + expect(await app.request("http://127.0.0.1:5387/v1/attach/manifest")).toHaveProperty( + "status", + 404, + ); + + await app.closeCapletsSessions(); + await engine.close(); + }); + + it("invokes exported attach entries by revision-scoped export ID", async () => { + const { engine } = testEngine({ + options: { exposure: "progressive" }, + }); + const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); + + const manifest = (await ( + await app.request("http://127.0.0.1:5387/v1/attach/manifest") + ).json()) as { + revision: string; + caplets: Array<{ exportId: string; kind: string; stableId: string; schemaHash: string }>; + }; + expect(manifest.caplets).toHaveLength(1); + + const invoked = await app.request("http://127.0.0.1:5387/v1/attach/invoke", { method: "POST", - headers: { - host: "127.0.0.1:5387", - accept: "application/json, text/event-stream", - "content-type": "application/json", - }, + headers: { "content-type": "application/json" }, body: JSON.stringify({ - jsonrpc: "2.0", - id: 1, - method: "initialize", - params: { - protocolVersion: "2025-03-26", - capabilities: {}, - clientInfo: { name: "test", version: "1.0.0" }, - }, + revision: manifest.revision, + kind: "caplet", + exportId: manifest.caplets[0]!.exportId, + input: { operation: "inspect" }, }), }); - expect(init.status).toBe(200); - const sessionId = init.headers.get("mcp-session-id"); - expect(sessionId).toBeTruthy(); + expect(invoked.status).toBe(200); + await expect(invoked.json()).resolves.toMatchObject({ + ok: true, + data: { + structuredContent: { + result: expect.objectContaining({ id: "status" }), + }, + }, + }); - await app.request("http://127.0.0.1:5387/mcp", { + const stale = await app.request("http://127.0.0.1:5387/v1/attach/invoke", { method: "POST", - headers: { - host: "127.0.0.1:5387", - accept: "application/json, text/event-stream", - "content-type": "application/json", - "mcp-session-id": sessionId!, - "mcp-protocol-version": "2025-03-26", - }, - body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }), + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + revision: "old", + kind: "caplet", + exportId: manifest.caplets[0]!.exportId, + input: { operation: "inspect" }, + }), + }); + expect(stale.status).toBe(409); + await expect(stale.json()).resolves.toMatchObject({ + ok: false, + error: { code: "ATTACH_MANIFEST_STALE" }, }); - const tools = await app.request("http://127.0.0.1:5387/mcp", { + const malformed = await app.request("http://127.0.0.1:5387/v1/attach/invoke", { method: "POST", - headers: { - host: "127.0.0.1:5387", - accept: "application/json, text/event-stream", - "content-type": "application/json", - "mcp-session-id": sessionId!, - "mcp-protocol-version": "2025-03-26", - }, - body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }), + headers: { "content-type": "application/json" }, + body: '{"revision":', + }); + expect(malformed.status).toBe(400); + await expect(malformed.json()).resolves.toMatchObject({ + ok: false, + error: { code: "REQUEST_INVALID" }, + }); + + await engine.close(); + }); + + it("recomputes attach projections before invokes so stale downstream surfaces are rejected", async () => { + const caplet = { + server: "docs", + name: "Docs", + description: "Docs.", + backend: "mcp", + command: process.execPath, + }; + let downstreamToolName = "read"; + const engine = { + onReload: () => () => undefined, + exposureSnapshot: async () => ({ + callableCaplets: [], + progressiveCaplets: [], + codeModeCaplets: [], + directTools: [ + { + caplet, + downstreamName: downstreamToolName, + name: `docs__${downstreamToolName}`, + tool: { name: downstreamToolName, inputSchema: { type: "object" } }, + }, + ], + directResources: [], + directResourceTemplates: [], + directPrompts: [], + hiddenCaplets: [], + }), + execute: async () => ({ called: true }), + } as unknown as CapletsEngine; + const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); + + const manifest = (await ( + await app.request("http://127.0.0.1:5387/v1/attach/manifest") + ).json()) as { + revision: string; + tools: Array<{ exportId: string; kind: string }>; + }; + downstreamToolName = "search"; + + const stale = await app.request("http://127.0.0.1:5387/v1/attach/invoke", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + revision: manifest.revision, + kind: "tool", + exportId: manifest.tools[0]!.exportId, + input: {}, + }), + }); + + expect(stale.status).toBe(409); + await expect(stale.json()).resolves.toMatchObject({ + ok: false, + error: { code: "ATTACH_MANIFEST_STALE" }, + }); + }); + + it("serves attach events as an unbuffered keep-alive SSE stream", async () => { + const { engine } = testEngine(); + const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); + + const response = await app.request("http://127.0.0.1:5387/v1/attach/events"); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("text/event-stream"); + expect(response.headers.get("cache-control")).toBe("no-cache"); + expect(response.headers.get("connection")).toBe("keep-alive"); + expect(response.headers.get("x-accel-buffering")).toBe("no"); + await response.body?.cancel(); + await engine.close(); + }); + + it("closes active attach event streams during app shutdown", async () => { + const { engine } = testEngine(); + const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); + + const response = await app.request("http://127.0.0.1:5387/v1/attach/events"); + const reader = response.body!.getReader(); + await expect(reader.read()).resolves.toMatchObject({ done: false }); + + await app.closeCapletsSessions(); + + await expect(reader.read()).resolves.toMatchObject({ done: true }); + await engine.close(); + }); + + it("rejects unauthenticated attach requests through public origin host by default", async () => { + const { engine } = testEngine(); + const app = createHttpServeApp( + httpOptions({ publicOrigin: "https://caplets.tail7ff085.ts.net" }), + engine, + { writeErr: () => {} }, + ); + + const response = await app.request("http://127.0.0.1:5387/v1/attach/manifest", { + headers: { host: "caplets.tail7ff085.ts.net" }, }); - expect(tools.status).toBe(200); - const body = await tools.text(); - expect(body).toContain("status"); + expect(response.status).toBe(403); + await engine.close(); + }); - const deleted = await app.request("http://127.0.0.1:5387/mcp", { - method: "DELETE", + it("allows authenticated attach requests through the configured public origin host", async () => { + const { engine } = testEngine(); + const password = "test-password"; + const app = createHttpServeApp( + httpOptions({ + publicOrigin: "https://caplets.tail7ff085.ts.net", + auth: { enabled: true, user: "caplets", password }, + }), + engine, + { writeErr: () => {} }, + ); + + const response = await app.request("http://127.0.0.1:5387/v1/attach/manifest", { headers: { - "mcp-session-id": sessionId!, - "mcp-protocol-version": "2025-03-26", - host: "127.0.0.1:5387", + host: "caplets.tail7ff085.ts.net", + authorization: `Basic ${Buffer.from(`caplets:${password}`).toString("base64")}`, }, }); - expect(deleted.status).toBe(200); + + expect(response.status).toBe(200); + await engine.close(); + }); + + it("does not expose unversioned service routes", async () => { + const { engine } = testEngine(); + const app = createHttpServeApp(httpOptions(), engine, { writeErr: () => {} }); + + for (const path of ["/mcp", "/control", "/attach", "/healthz"]) { + const response = await app.request(`http://127.0.0.1:5387${path}`, { method: "POST" }); + expect(response.status).toBe(404); + } await engine.close(); }); @@ -541,7 +809,7 @@ describe("createHttpServeApp", () => { { writeErr: () => {} }, ); - const init = await app.request("http://127.0.0.1:5387/mcp", { + const init = await app.request("http://127.0.0.1:5387/v1/mcp", { method: "POST", headers: { host: "caplets.tail7ff085.ts.net", @@ -577,7 +845,7 @@ describe("createHttpServeApp", () => { { writeErr: () => {} }, ); - const init = await app.request("http://127.0.0.1:5387/mcp", { + const init = await app.request("http://127.0.0.1:5387/v1/mcp", { method: "POST", headers: { host: "caplets.tail7ff085.ts.net", @@ -614,7 +882,7 @@ describe("createHttpServeApp", () => { { writeErr: () => {} }, ); - const init = await app.request("http://127.0.0.1:5387/mcp", { + const init = await app.request("http://127.0.0.1:5387/v1/mcp", { method: "POST", headers: { host: "caplets.tail7ff085.ts.net", @@ -640,6 +908,90 @@ describe("createHttpServeApp", () => { }); }); +async function listMcpTools( + app: ReturnType, + path: "/v1/mcp", +): Promise> { + const init = await app.request(`http://127.0.0.1:5387${path}`, { + method: "POST", + headers: { + host: "127.0.0.1:5387", + accept: "application/json, text/event-stream", + "content-type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-03-26", + capabilities: {}, + clientInfo: { name: "test", version: "1.0.0" }, + }, + }), + }); + + expect(init.status).toBe(200); + const sessionId = init.headers.get("mcp-session-id"); + expect(sessionId).toBeTruthy(); + + await app.request(`http://127.0.0.1:5387${path}`, { + method: "POST", + headers: { + host: "127.0.0.1:5387", + accept: "application/json, text/event-stream", + "content-type": "application/json", + "mcp-session-id": sessionId!, + "mcp-protocol-version": "2025-03-26", + }, + body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }), + }); + + const response = await app.request(`http://127.0.0.1:5387${path}`, { + method: "POST", + headers: { + host: "127.0.0.1:5387", + accept: "application/json, text/event-stream", + "content-type": "application/json", + "mcp-session-id": sessionId!, + "mcp-protocol-version": "2025-03-26", + }, + body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }), + }); + + expect(response.status).toBe(200); + const payload = parseMcpResponse(await response.text()); + + const deleted = await app.request(`http://127.0.0.1:5387${path}`, { + method: "DELETE", + headers: { + "mcp-session-id": sessionId!, + "mcp-protocol-version": "2025-03-26", + host: "127.0.0.1:5387", + }, + }); + expect(deleted.status).toBe(200); + + return payload.result.tools; +} + +function parseMcpResponse(text: string): { + result: { tools: Array<{ name: string; inputSchema?: unknown }> }; +} { + if (text.trimStart().startsWith("{")) { + return JSON.parse(text) as { + result: { tools: Array<{ name: string; inputSchema?: unknown }> }; + }; + } + const dataLine = text.split("\n").find((line) => line.startsWith("data:")); + if (!dataLine) { + throw new Error(`Could not parse MCP response: ${text}`); + } + return JSON.parse(dataLine.slice("data:".length).trim()) as { + result: { tools: Array<{ name: string; inputSchema?: unknown }> }; + }; +} + function httpOptions(overrides: Partial = {}): HttpServeOptions { return { transport: "http", @@ -656,8 +1008,8 @@ function httpOptions(overrides: Partial = {}): HttpServeOption }; } -function testEngine(): { engine: CapletsEngine } { - const context = testContext(); +function testEngine(config: Record = {}): { engine: CapletsEngine } { + const context = testContext(config); return { engine: new CapletsEngine({ configPath: context.configPath, @@ -667,7 +1019,7 @@ function testEngine(): { engine: CapletsEngine } { }; } -function testContext(options: { oauth?: boolean } = {}): { +function testContext(options: { oauth?: boolean } & Record = {}): { configPath: string; projectConfigPath: string; projectCapletsRoot: string; @@ -685,6 +1037,7 @@ function testContext(options: { oauth?: boolean } = {}): { JSON.stringify( options.oauth ? { + ...(options.options ? { options: options.options } : {}), httpApis: { remote: { name: "Remote", @@ -701,6 +1054,7 @@ function testContext(options: { oauth?: boolean } = {}): { }, } : { + ...(options.options ? { options: options.options } : {}), httpApis: { status: { name: "Status", diff --git a/packages/core/test/server-options.test.ts b/packages/core/test/server-options.test.ts index 91cb0cac..b4d1529e 100644 --- a/packages/core/test/server-options.test.ts +++ b/packages/core/test/server-options.test.ts @@ -60,9 +60,10 @@ describe("resolveCapletsServer", () => { expect(resolved).toMatchObject({ baseUrl: new URL("https://example.com/caplets"), - mcpUrl: new URL("https://example.com/caplets/mcp"), - controlUrl: new URL("https://example.com/caplets/control"), - healthUrl: new URL("https://example.com/caplets/healthz"), + mcpUrl: new URL("https://example.com/caplets/v1/mcp"), + attachUrl: new URL("https://example.com/caplets/v1/attach"), + controlUrl: new URL("https://example.com/caplets/v1/admin"), + healthUrl: new URL("https://example.com/caplets/v1/healthz"), auth: { enabled: true, user: "caplets", password }, requestInit: { headers: { @@ -75,18 +76,20 @@ describe("resolveCapletsServer", () => { it("derives service URLs from a root URL", () => { expect(resolveCapletsServer({ url: "https://example.com" }, {})).toMatchObject({ baseUrl: new URL("https://example.com/"), - mcpUrl: new URL("https://example.com/mcp"), - controlUrl: new URL("https://example.com/control"), - healthUrl: new URL("https://example.com/healthz"), + mcpUrl: new URL("https://example.com/v1/mcp"), + attachUrl: new URL("https://example.com/v1/attach"), + controlUrl: new URL("https://example.com/v1/admin"), + healthUrl: new URL("https://example.com/v1/healthz"), }); }); it("accepts loopback http IPv6 bracket URLs", () => { expect(resolveCapletsServer({ url: "http://[::1]:5387" }, {})).toMatchObject({ baseUrl: new URL("http://[::1]:5387/"), - mcpUrl: new URL("http://[::1]:5387/mcp"), - controlUrl: new URL("http://[::1]:5387/control"), - healthUrl: new URL("http://[::1]:5387/healthz"), + mcpUrl: new URL("http://[::1]:5387/v1/mcp"), + attachUrl: new URL("http://[::1]:5387/v1/attach"), + controlUrl: new URL("http://[::1]:5387/v1/admin"), + healthUrl: new URL("http://[::1]:5387/v1/healthz"), }); }); diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ee1e268d..4af0b525 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -38,9 +38,9 @@ }, "devDependencies": { "@jitl/quickjs-wasmfile-release-sync": "^0.32.0", - "@types/node": "^25.9.1", - "@typescript/native-preview": "7.0.0-dev.20260603.1", - "rolldown": "^1.0.3", + "@types/node": "^25.9.3", + "@typescript/native-preview": "7.0.0-dev.20260613.1", + "rolldown": "^1.1.1", "typescript": "^6.0.3", "vitest": "^4.1.8" }, @@ -48,6 +48,6 @@ "@opencode-ai/plugin": ">=1" }, "engines": { - "node": ">=22" + "node": ">=24" } } diff --git a/packages/pi/package.json b/packages/pi/package.json index 91edd531..0af841f1 100644 --- a/packages/pi/package.json +++ b/packages/pi/package.json @@ -38,9 +38,9 @@ }, "devDependencies": { "@jitl/quickjs-wasmfile-release-sync": "^0.32.0", - "@types/node": "^25.9.1", - "@typescript/native-preview": "7.0.0-dev.20260603.1", - "rolldown": "^1.0.3", + "@types/node": "^25.9.3", + "@typescript/native-preview": "7.0.0-dev.20260613.1", + "rolldown": "^1.1.1", "typescript": "^6.0.3", "vitest": "^4.1.8" }, @@ -49,7 +49,7 @@ "@earendil-works/pi-tui": "*" }, "engines": { - "node": ">=22" + "node": ">=24" }, "pi": { "extensions": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67c71b64..42f1957f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,19 +10,19 @@ importers: devDependencies: '@changesets/cli': specifier: ^2.31.0 - version: 2.31.0(@types/node@25.9.2) + version: 2.31.0(@types/node@25.9.3) '@cloudflare/workers-types': - specifier: ^4.20260610.1 - version: 4.20260610.1 + specifier: ^4.20260613.1 + version: 4.20260613.1 '@types/node': - specifier: ^25.9.2 - version: 25.9.2 + specifier: ^25.9.3 + version: 25.9.3 '@typescript/native-preview': - specifier: 7.0.0-dev.20260610.1 - version: 7.0.0-dev.20260610.1 + specifier: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260613.1 alchemy: specifier: 0.93.12 - version: 0.93.12(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(workerd@1.20260601.1) + version: 0.93.12(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(workerd@1.20260611.1) husky: specifier: ^9.1.7 version: 9.1.7 @@ -39,38 +39,38 @@ importers: specifier: ^0.14.1 version: 0.14.1 rolldown: - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^1.1.1 + version: 1.1.1 tsx: specifier: ^4.22.4 version: 4.22.4 turbo: - specifier: ^2.9.17 - version: 2.9.17 + specifier: ^2.9.18 + version: 2.9.18 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.2)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.8(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) apps/landing: dependencies: '@astrojs/check': specifier: ^0.9.9 - version: 0.9.9(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@6.0.3) + version: 0.9.9(prettier-plugin-astro@0.14.1)(prettier@3.8.4)(typescript@6.0.3) '@hugeicons/core-free-icons': specifier: 4.2.0 version: 4.2.0 '@tailwindcss/vite': - specifier: ^4.3.0 - version: 4.3.0(vite@7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + specifier: ^4.3.1 + version: 4.3.1(vite@7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) astro: - specifier: ^6.4.3 - version: 6.4.3(@types/node@25.9.2)(aws4fetch@1.0.20)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.0)(tsx@4.22.4)(yaml@2.9.0) + specifier: ^6.4.6 + version: 6.4.6(@types/node@25.9.3)(aws4fetch@1.0.20)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0) tailwindcss: - specifier: ^4.3.0 - version: 4.3.0 + specifier: ^4.3.1 + version: 4.3.1 three: specifier: ^0.184.0 version: 0.184.0 @@ -83,7 +83,7 @@ importers: version: 0.184.1 vite: specifier: ^7.3.5 - version: 7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) + version: 7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) packages/benchmarks: dependencies: @@ -94,15 +94,15 @@ importers: specifier: ^15.0.0 version: 15.0.0 js-tiktoken: - specifier: ^1.0.20 + specifier: ^1.0.21 version: 1.0.21 devDependencies: '@types/node': - specifier: ^25.9.1 - version: 25.9.1 + specifier: ^25.9.3 + version: 25.9.3 '@typescript/native-preview': - specifier: 7.0.0-dev.20260603.1 - version: 7.0.0-dev.20260603.1 + specifier: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260613.1 caplets: specifier: workspace:* version: link:../cli @@ -111,7 +111,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.8(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/cli: dependencies: @@ -123,32 +123,35 @@ importers: version: 1.29.0(zod@4.4.3) devDependencies: '@types/node': - specifier: ^25.9.1 - version: 25.9.1 + specifier: ^25.9.3 + version: 25.9.3 '@typescript/native-preview': - specifier: 7.0.0-dev.20260603.1 - version: 7.0.0-dev.20260603.1 + specifier: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260613.1 rolldown: - specifier: ^1.0.3 - version: 1.0.3 + specifier: ^1.1.1 + version: 1.1.1 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.8(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/core: dependencies: '@apidevtools/swagger-parser': specifier: ^12.1.0 version: 12.1.0(openapi-types@12.1.3) + '@babel/parser': + specifier: ^7.29.7 + version: 7.29.7 '@hono/mcp': specifier: ^0.3.0 - version: 0.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(hono-rate-limiter@0.5.3(hono@4.12.23)(unstorage@1.17.5(aws4fetch@1.0.20)))(hono@4.12.23)(zod@4.4.3) + version: 0.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(hono-rate-limiter@0.5.3(hono@4.12.25)(unstorage@1.17.5(aws4fetch@1.0.20)))(hono@4.12.25)(zod@4.4.3) '@hono/node-server': specifier: ^2.0.4 - version: 2.0.4(hono@4.12.23) + version: 2.0.4(hono@4.12.25) '@modelcontextprotocol/sdk': specifier: ^1.29.0 version: 1.29.0(zod@4.4.3) @@ -159,11 +162,11 @@ importers: specifier: ^15.0.0 version: 15.0.0 graphql: - specifier: ^16.14.1 - version: 16.14.1 + specifier: ^16.14.2 + version: 16.14.2 hono: - specifier: ^4.12.23 - version: 4.12.23 + specifier: ^4.12.25 + version: 4.12.25 quickjs-emscripten: specifier: ^0.32.0 version: 0.32.0 @@ -184,17 +187,17 @@ importers: version: 4.4.3 devDependencies: '@types/node': - specifier: ^25.9.1 - version: 25.9.1 + specifier: ^25.9.3 + version: 25.9.3 '@typescript/native-preview': - specifier: 7.0.0-dev.20260603.1 - version: 7.0.0-dev.20260603.1 + specifier: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260613.1 rolldown: - specifier: ^1.0.3 - version: 1.0.3 + specifier: ^1.1.1 + version: 1.1.1 vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.8(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/opencode: dependencies: @@ -209,20 +212,20 @@ importers: specifier: ^0.32.0 version: 0.32.0 '@types/node': - specifier: ^25.9.1 - version: 25.9.1 + specifier: ^25.9.3 + version: 25.9.3 '@typescript/native-preview': - specifier: 7.0.0-dev.20260603.1 - version: 7.0.0-dev.20260603.1 + specifier: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260613.1 rolldown: - specifier: ^1.0.3 - version: 1.0.3 + specifier: ^1.1.1 + version: 1.1.1 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.8(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/pi: dependencies: @@ -240,20 +243,20 @@ importers: specifier: ^0.32.0 version: 0.32.0 '@types/node': - specifier: ^25.9.1 - version: 25.9.1 + specifier: ^25.9.3 + version: 25.9.3 '@typescript/native-preview': - specifier: 7.0.0-dev.20260603.1 - version: 7.0.0-dev.20260603.1 + specifier: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260613.1 rolldown: - specifier: ^1.0.3 - version: 1.0.3 + specifier: ^1.1.1 + version: 1.1.1 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.8(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages: @@ -344,52 +347,88 @@ packages: resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-cognito-identity@3.1060.0': - resolution: {integrity: sha512-JS3YVzOnwLYD+OzAYVXVrP/IzaQrW8c9c/pI2eehsltvqSrl9pyNu5RzcgAxy79zHwv5f21j6FBWcgQ7Bj9eHA==} + '@aws-sdk/client-cognito-identity@3.1068.0': + resolution: {integrity: sha512-by2Qj3f9BI9X4cY0n160R3uzkMpI6k9PmGA8QLAuzr8HzkiNrYFygMEPhIEdqBFHQPD/8AXNUs45HYjEDsQ33g==} engines: {node: '>=20.0.0'} '@aws-sdk/core@3.974.17': resolution: {integrity: sha512-r8o4h2K7j6P9ngno+8ei0aK0U/4JwDb7A2fMMxGVoSqDN8AFlIzSDeZHME9LcVLR2codyhtr1WAAg+/nmkeeMA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-cognito-identity@3.972.40': - resolution: {integrity: sha512-viZSv9qWrcfWvpnhy8FPQ5y0ee4TiCOX/xM8LSQXR+xJn0P4DV7B+XwXfOZ+Ik3+yQ/aO69qVnngEpXExAwizA==} + '@aws-sdk/core@3.974.20': + resolution: {integrity: sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-cognito-identity@3.972.45': + resolution: {integrity: sha512-4L/REssieLON1hVsTzZucP1p2u5jmPNejyh/9BCGZXr93IalBbhRPCrtKIKwMTu9yRGr/bcKzhrQocByKLSzLQ==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-env@3.972.43': resolution: {integrity: sha512-g0XVQKzaA/4cq1vz1IvCQwYM+1Pkv01J9yHDpCTXekVuGZRDEz0wqBQ1AuYTq7FM6uik4uBGH8Tb5d9YvgeA7g==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.46': + resolution: {integrity: sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.45': resolution: {integrity: sha512-w9PuOoKCt6+xoESvY+zlV0u3PKQ0mVL259PcsVR6a3S/uYJJHnIi4r1NxdJHEcNldUVRIciltWnFMGBR4YEm3g==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.48': + resolution: {integrity: sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.972.48': resolution: {integrity: sha512-+6BQ6Lrnc+EyAGElLRW6j+Sa+RirPHnIJsobvYO6nnyK+oGKmz1ne/ieclbLWyjyDKEU3/JVJWcWY3VLFPvGtQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.972.53': + resolution: {integrity: sha512-ZfdhIOR41q8TcWEnUac+gCOb+O2LBWdHLmjedXpXz4IEFW2ppNuFcm6p0sMTavpM+zD5TYfpH5Gp7guRyqSgsQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.47': resolution: {integrity: sha512-Iy2ebWVgrZBH05464uJiQYu6HSSiROnwVZptthEFXx2gWjo1ORCxEAFZB5Cr2MdfrSnZ+0QUPkZ1ZpCqpkUrLQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.52': + resolution: {integrity: sha512-9hu2oR0qH7Fst5Tzdx+UWxm+w5zCXtErTLtOOW5hwwQc170CLwOeniRxyFY6s9mHfGEfC5zFukNBdKBwJR8mhQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.50': resolution: {integrity: sha512-b05Aelq5cqAvCCDQjCYacl0XmR8QhBNSqLbsdISkQmlQBa5oPS66zYPteWcSp5LswbpoIe552EUGjluKiadBig==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.55': + resolution: {integrity: sha512-zMGLa/dhESVqmCD7mmIFFKSwSFrJGScvCXcjvBZEVOOMauFS5JRQvLTMukFpMEFWiV6dTAlsen2ATDBulLPtbg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.43': resolution: {integrity: sha512-GPokLNyvTfCmuaHk+v3GKVs4ZT3cMu5kgS2a+NPkOMt96cq6fSIK0g+mZHpGS6Cd4QGrPKesANEaLUKgOskTzg==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.46': + resolution: {integrity: sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.972.47': resolution: {integrity: sha512-0AzvLrzlvJs0DzbeWGvNj+bX3Uzd7VNS6vDqCOdZzBlCGKGd78uxctJSW9iK/Rt/nxiJqpTvrYQlVJ4guVM2Dw==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.972.52': + resolution: {integrity: sha512-nb2/n4o/HQf+FVpVbZe9vCTFngmuDoIsltMgLAtjixaKzvzhB4J8WSDFyWgnErgLHk55ctWH+I4PU+LIHhyffg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.47': resolution: {integrity: sha512-eksfbUErOejUAGWBAcNqaP7IX21oUOEo73d9R56k9Ua4d57qS90NEYkWJsuSGzTXMFulCu17qXJI/qGmM7hvoA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-providers@3.1060.0': - resolution: {integrity: sha512-fcazJa+YQTJo+Cxc8K7IOTHpEzc2vUBKnchjNpqr9rLcTKGXajl3U0SByi4YDo89AC4WP2d6LqQaTNysuWMNhA==} + '@aws-sdk/credential-provider-web-identity@3.972.52': + resolution: {integrity: sha512-lKj6aRSGbqLmpYmM24bY7a1Xmfcq2vkE3hv8CSPYfc1yCu0BPu/XEJ1L4Fm61MsU6ULLNSG8UGsffNoFUBjESA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-providers@3.1068.0': + resolution: {integrity: sha512-DN7UewD/XKGfNGWXZsTECWRj3IsULkE7aG+fPwqPf13CnX4UmNvj80g1xieJ0lVdMQ2h0L3gZpn2ClIY06+/vQ==} engines: {node: '>=20.0.0'} '@aws-sdk/eventstream-handler-node@3.972.18': @@ -408,10 +447,18 @@ packages: resolution: {integrity: sha512-Fpri1/PXKMKveORZ7E00VLTlWS5DkfZkW70PUE+bOnpWpAeHAQLoiDHhkzN3kNWbbSsGg64+IZYiq/EZgME3Mg==} engines: {node: '>=20.0.0'} + '@aws-sdk/nested-clients@3.997.20': + resolution: {integrity: sha512-IYJuLpXp2DEILVQpQOy0PMpkftv0AHEOCn52o0atyOaumA0CdWQ3klPyXdViGYLbNpESsVFMVybvHUeZAuiGxA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.31': resolution: {integrity: sha512-Kn2up9SlG1KC6wRtwf0d7waTGF6rvp9DxYqB54x6UCKdQ6kyaXCqHL4WGb5vUJga5kS8FxnjhY0LqM28aMvnNQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.34': + resolution: {integrity: sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1048.0': resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} engines: {node: '>=20.0.0'} @@ -420,18 +467,30 @@ packages: resolution: {integrity: sha512-6NZaMKkFhpaNiwLpHi1sZaYjidL/lCJE6ME6NxwA8gv9vQna+Kr0j4OFwVoz6tANRWM3WbGz6jiPsGX/Vkjwow==} engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1066.0': + resolution: {integrity: sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.10': resolution: {integrity: sha512-992QrTO7G9qCvKD0fx1rMlqcL14plUcRAbwmqqYVsuF3GrqcvlAL9qxR+baMafarEZ+l7DUQ5lCMmt5mbMhF7g==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-locate-window@3.965.5': - resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + '@aws-sdk/types@3.973.12': + resolution: {integrity: sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.7': + resolution: {integrity: sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA==} engines: {node: '>=20.0.0'} '@aws-sdk/xml-builder@3.972.27': resolution: {integrity: sha512-hpsCXCOI436kxWpjtRuIHVvuPP81MOw8f18jzfZeg+UOiiOvlqWcmWChzEhJEu16cOC6+ku4ncBN+7rdt+DZ9g==} engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.29': + resolution: {integrity: sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ==} + engines: {node: '>=20.0.0'} + '@aws/lambda-invoke-store@0.2.4': resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} @@ -457,8 +516,8 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@capsizecss/unpack@4.0.0': - resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} + '@capsizecss/unpack@4.0.1': + resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} engines: {node: '>=18'} '@changesets/apply-release-plan@7.1.1': @@ -516,12 +575,12 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@clack/core@1.4.0': - resolution: {integrity: sha512-7Wctjq6f7c1CPz8sPpkwUnz8yRgVANkpNupb81q432FjcJg4l+Sw7XANdNSdWfAKq0IHI0JTcUeK5dxs/HrGPw==} + '@clack/core@1.4.1': + resolution: {integrity: sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==} engines: {node: '>= 20.12.0'} - '@clack/prompts@1.5.0': - resolution: {integrity: sha512-wKh+wTjmrUoUdkZg8KpJO5X+p9PWV+KE9mePseq9UYWkukgTKsGS47RRL2HstwVcvDQH+PenrPJWII8+MfiiyA==} + '@clack/prompts@1.5.1': + resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==} engines: {node: '>= 20.12.0'} '@cloudflare/kv-asset-handler@0.5.0': @@ -552,8 +611,8 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260601.1': - resolution: {integrity: sha512-iXZBVuRbvuVqQ/63wul01hHCv/3R8G5S8zbkjfoHvyPZFynmlKTV59Hk+H8whyGwFAZuB71UJGLr+G5mJKfjWA==} + '@cloudflare/workerd-darwin-64@1.20260611.1': + resolution: {integrity: sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -564,8 +623,8 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260601.1': - resolution: {integrity: sha512-veGpZQGBw07Twt+Y4z3oyo+/obKHt0iWUwvDV5GOiDAYjC/zW+YGstgVzg4SHq+k1sLH3ElqL2TXx20I5WBv3Q==} + '@cloudflare/workerd-darwin-arm64@1.20260611.1': + resolution: {integrity: sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -576,8 +635,8 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260601.1': - resolution: {integrity: sha512-n/9hDz7fPGpYF0J684+Xr5zgjcS2jdmY2Of5m6e+eQ/M9+RfR+UaU8Ee/tkA1dDC0LYQB13hfPafZG66Ff1CsA==} + '@cloudflare/workerd-linux-64@1.20260611.1': + resolution: {integrity: sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -588,8 +647,8 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260601.1': - resolution: {integrity: sha512-VHRZZbexATS+n+1j3x/CZaYbIJEye0J3iIHgG0Wp+l+NrZCKQ8qi8Lq1uTV0dLJQ67FuZtJtWdQ95mm9F7Fc+A==} + '@cloudflare/workerd-linux-arm64@1.20260611.1': + resolution: {integrity: sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -600,17 +659,14 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260601.1': - resolution: {integrity: sha512-ye0C7MFLkeH16iTo8Tcjv2KiFmp23+sZGvUzSQa4xhP0QMe6EoJ+H/4SqqvnZ5nfN54slqKvx2VnXceENWe2CQ==} + '@cloudflare/workerd-windows-64@1.20260611.1': + resolution: {integrity: sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20260603.1': - resolution: {integrity: sha512-TLeVHoBbcYv35S5TdRWUoj3IJ56BhHtrsuci+O7ithU8yz7ttNdCk6rAl1QUSGNVEWSIp54bWOuV/xmX1zu79g==} - - '@cloudflare/workers-types@4.20260610.1': - resolution: {integrity: sha512-Mk/f3lUygeIHzQ4HnJjU/JvGg/kllgp9gISty9nylHE/2M2MFeKO+hgAKSgiPpmwUbuhewdYGgqFGgT/ADK0/g==} + '@cloudflare/workers-types@4.20260613.1': + resolution: {integrity: sha512-1mrgjE6epolwBhroeGAp5ud5H6Vyi6tl1o/NP0T4rXJ8bmEjmhHnbCzAhHTDHV0PIeip43wcuzHKJarvaGTaUA==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -661,12 +717,24 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.0': + resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.0': + resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -685,8 +753,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -709,8 +777,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -733,8 +801,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -757,8 +825,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -781,8 +849,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -805,8 +873,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -829,8 +897,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -853,8 +921,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -877,8 +945,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -901,8 +969,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -925,8 +993,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -949,8 +1017,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -973,8 +1041,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -997,8 +1065,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -1021,8 +1089,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -1045,8 +1113,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -1069,8 +1137,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -1093,8 +1161,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -1117,8 +1185,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -1141,8 +1209,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1165,8 +1233,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -1189,8 +1257,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -1213,8 +1281,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -1237,8 +1305,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -1261,8 +1329,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -1285,8 +1353,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1643,14 +1711,14 @@ packages: cpu: [x64] os: [win32] - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@nodable/entities@2.1.1': - resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==} + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1745,8 +1813,8 @@ packages: '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - '@oxc-project/types@0.134.0': - resolution: {integrity: sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==} + '@oxc-project/types@0.135.0': + resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==} '@oxfmt/binding-android-arm-eabi@0.54.0': resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==} @@ -2041,8 +2109,8 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.1.0': - resolution: {integrity: sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA==} + '@rolldown/binding-android-arm64@1.1.1': + resolution: {integrity: sha512-BLf9Wak/gfwVb7NQTQW4wBgL3oAfPy7ArEkhwV543OVw/uY6B47z5xYsqPSZ9PDOorvURPinws6ThaFuNgGLgA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -2053,8 +2121,8 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.1.0': - resolution: {integrity: sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w==} + '@rolldown/binding-darwin-arm64@1.1.1': + resolution: {integrity: sha512-rRZRPy/Ynb+Mxu0O6tfPldHeDgAn0sRij+IOUy6sFdUlv3hArGW/DloE3GfAxtqpOJuRNgF74Nr5gM4xBeU2jQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -2065,8 +2133,8 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.0': - resolution: {integrity: sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA==} + '@rolldown/binding-darwin-x64@1.1.1': + resolution: {integrity: sha512-/MtefPxhKPyWWFM8L45OWiEqRf+eSU2Qv9ZAyTaoZOoGcoPKxbbhjTJO2/U2IThv0uDZ4NWHc3/oTsR6IEOtww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -2077,8 +2145,8 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.1.0': - resolution: {integrity: sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ==} + '@rolldown/binding-freebsd-x64@1.1.1': + resolution: {integrity: sha512-202K+cpIi1kx/Zn7AtxBi4LTXSY67Aszb2K9rNsuW7FeBeh0nqoNmYLOSZidV0p88VPBzMmTZcHAdPNo3kRYzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -2089,8 +2157,8 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.1.0': - resolution: {integrity: sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.1': + resolution: {integrity: sha512-wl9NfeXNUwrXtUc063tddmZFUI6qiNs1CNOwni0OL4vC7MqVSYugra3ZgtDmtVy8e0DluJTENmzIv2BwqLzT4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -2102,8 +2170,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.1.0': - resolution: {integrity: sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg==} + '@rolldown/binding-linux-arm64-gnu@1.1.1': + resolution: {integrity: sha512-at2EO4o7D/PJLC4Xik16bU4CcjQE2tSv1LfqMA0TRYQYQihRm3gZeDB8xaX28A9SFedibcAk5DeMCKt4REKG0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -2116,8 +2184,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.1.0': - resolution: {integrity: sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ==} + '@rolldown/binding-linux-arm64-musl@1.1.1': + resolution: {integrity: sha512-5PUjZx366h9tkJTPJF5eibxOlK3sGoeRiBJLLjjEB5/kLDuhr6qB3LkhqLz1smXNgsX+pBhnbcJBrPE30HznAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -2130,8 +2198,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.1.0': - resolution: {integrity: sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ==} + '@rolldown/binding-linux-ppc64-gnu@1.1.1': + resolution: {integrity: sha512-1WK84XPeio3tjP1sM/TMXiC0G1i1iq1qGZ71KfNQjEFLU1kwD+Cv5T8nGySg/JUFwLbaScu6ve9DmeXlmqpkFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] @@ -2144,8 +2212,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.0': - resolution: {integrity: sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ==} + '@rolldown/binding-linux-s390x-gnu@1.1.1': + resolution: {integrity: sha512-1nS1X5z1uMJ369RU25hTpKCFvUwXZp12dIzlzk4S+UxCTcSVGsAE6tzkOSufv/7jnmAtK0ZlrsJxh2fGmsnVSw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -2158,8 +2226,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.0': - resolution: {integrity: sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg==} + '@rolldown/binding-linux-x64-gnu@1.1.1': + resolution: {integrity: sha512-NwX/wspnq4vYyMFsqbYvzums3ki/Tk8FZbMzMAovPDp3OfLeYKby/D+9osokadXuYEV3OvpeHlwnr/bG8QMixA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -2172,8 +2240,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.1.0': - resolution: {integrity: sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ==} + '@rolldown/binding-linux-x64-musl@1.1.1': + resolution: {integrity: sha512-+n46LhDrJFQM+229y4oXtVpj1G50U/+XuHMlpnisFTEXhrg9f/YIjp/HymX+PVJjBEr7XHRs3CFLelV464pqwA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -2185,8 +2253,8 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.1.0': - resolution: {integrity: sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg==} + '@rolldown/binding-openharmony-arm64@1.1.1': + resolution: {integrity: sha512-qGwEu47zOWYo7LdRHhCWTNhzwGtxXpdY6CERs8QEOqC0PXGGics/e3vHnyEUKt8xK6YkbZXFUCeklrpB6js8ag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -2196,8 +2264,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.1.0': - resolution: {integrity: sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ==} + '@rolldown/binding-wasm32-wasi@1.1.1': + resolution: {integrity: sha512-qczfgEH8u0wHGGOXtA7UMAybNKuQjjEXairyQaw4WzjiMztfbgatG1h4OKays/smhtwbWltpKCRGtVhU6h40Sg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] @@ -2207,8 +2275,8 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.1.0': - resolution: {integrity: sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg==} + '@rolldown/binding-win32-arm64-msvc@1.1.1': + resolution: {integrity: sha512-4psXSh63mSbwJF+mB8/9yfUUEzBiHYcUjxa32EO9ZwKy0Ypwjcg4F10D8SvVXgd+isy2UUUjF9HJJnDu1T/4Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -2219,8 +2287,8 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.0': - resolution: {integrity: sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w==} + '@rolldown/binding-win32-x64-msvc@1.1.1': + resolution: {integrity: sha512-MUvC/HLXVjzkQkWiExdVTEEWf0py+GfWm8WKSZsekG3ih6a21iy0BHPF07X3JIf3ifoklZXTIaHTLPBgH1C3dw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2237,141 +2305,141 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.61.0': - resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} + '@rollup/rollup-android-arm-eabi@4.62.0': + resolution: {integrity: sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.61.0': - resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} + '@rollup/rollup-android-arm64@4.62.0': + resolution: {integrity: sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.61.0': - resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} + '@rollup/rollup-darwin-arm64@4.62.0': + resolution: {integrity: sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.61.0': - resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} + '@rollup/rollup-darwin-x64@4.62.0': + resolution: {integrity: sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.61.0': - resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} + '@rollup/rollup-freebsd-arm64@4.62.0': + resolution: {integrity: sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.61.0': - resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} + '@rollup/rollup-freebsd-x64@4.62.0': + resolution: {integrity: sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.61.0': - resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} + '@rollup/rollup-linux-arm-gnueabihf@4.62.0': + resolution: {integrity: sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.61.0': - resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} + '@rollup/rollup-linux-arm-musleabihf@4.62.0': + resolution: {integrity: sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.61.0': - resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} + '@rollup/rollup-linux-arm64-gnu@4.62.0': + resolution: {integrity: sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.61.0': - resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} + '@rollup/rollup-linux-arm64-musl@4.62.0': + resolution: {integrity: sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.61.0': - resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} + '@rollup/rollup-linux-loong64-gnu@4.62.0': + resolution: {integrity: sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.61.0': - resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} + '@rollup/rollup-linux-loong64-musl@4.62.0': + resolution: {integrity: sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.61.0': - resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} + '@rollup/rollup-linux-ppc64-gnu@4.62.0': + resolution: {integrity: sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.61.0': - resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} + '@rollup/rollup-linux-ppc64-musl@4.62.0': + resolution: {integrity: sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.61.0': - resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} + '@rollup/rollup-linux-riscv64-gnu@4.62.0': + resolution: {integrity: sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.61.0': - resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} + '@rollup/rollup-linux-riscv64-musl@4.62.0': + resolution: {integrity: sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.61.0': - resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} + '@rollup/rollup-linux-s390x-gnu@4.62.0': + resolution: {integrity: sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.61.0': - resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} + '@rollup/rollup-linux-x64-gnu@4.62.0': + resolution: {integrity: sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.61.0': - resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} + '@rollup/rollup-linux-x64-musl@4.62.0': + resolution: {integrity: sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.61.0': - resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} + '@rollup/rollup-openbsd-x64@4.62.0': + resolution: {integrity: sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.61.0': - resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} + '@rollup/rollup-openharmony-arm64@4.62.0': + resolution: {integrity: sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.61.0': - resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} + '@rollup/rollup-win32-arm64-msvc@4.62.0': + resolution: {integrity: sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.61.0': - resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} + '@rollup/rollup-win32-ia32-msvc@4.62.0': + resolution: {integrity: sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.61.0': - resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} + '@rollup/rollup-win32-x64-gnu@4.62.0': + resolution: {integrity: sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.61.0': - resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} + '@rollup/rollup-win32-x64-msvc@4.62.0': + resolution: {integrity: sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==} cpu: [x64] os: [win32] @@ -2424,10 +2492,18 @@ packages: resolution: {integrity: sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==} engines: {node: '>=18.0.0'} + '@smithy/core@3.24.7': + resolution: {integrity: sha512-KoUi4M1f3BG6kzN1FnCwL7oyFptTbyBJKjR6yhSib+JHRdUmM1o+VwsFtJ66NZCkCzVfJMWRHJNo0R0jznp0Pg==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.3.7': resolution: {integrity: sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg==} engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.3.9': + resolution: {integrity: sha512-ZlfJ/4Fa3jYb+3eaohPfG9utX9HmdhFNcFtpoGAhUhdynAOmGXtmigbi7eEiONKM+ykHw8RwKuDEb85Lx7t7fA==} + engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.4.5': resolution: {integrity: sha512-SK3VMeH0fibgdTg2QeB+O4p7Yy/2E5HBOHJeC58FshkDdeuX8lOgO7PfjYfLyPLP1ch55j91cQqKBzDS0mRjSQ==} engines: {node: '>=18.0.0'} @@ -2436,12 +2512,16 @@ packages: resolution: {integrity: sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==} engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.4.7': + resolution: {integrity: sha512-NslaM2ir0N2hisDmzXLstPaVINZheh8SokyOC++kzFPloZucL2R7Y7bS57mSzx/1Fc/fqmn7twjkeezTTrV0EA==} + engines: {node: '>=18.0.0'} + '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} - '@smithy/node-config-provider@4.4.6': - resolution: {integrity: sha512-M+gG6eQ0y073mSmNB+erRXJvwpsqsN72ol2w6vcd8FEKeG7pqYK0JvzfVqONkPj2ElBB2pg+cU13I850b//Wag==} + '@smithy/node-config-provider@4.4.7': + resolution: {integrity: sha512-GbLL8AyUmsYYmWBNa4YU4vlSDG2kBeLp6Sevh20dvnfRYsgl+eGR0dv9X5wKFDURiVZCruz6OgfM1AQYOHM04Q==} engines: {node: '>=18.0.0'} '@smithy/node-http-handler@4.7.3': @@ -2452,14 +2532,26 @@ packages: resolution: {integrity: sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ==} engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.7.8': + resolution: {integrity: sha512-f+DbsWUwSbtMu1a/j8Y93KiU1SRg9nyzfjereqn1BJ33QOTUXxdlYvVXMhAYl1vuR1Kmna5aIJe09KSIfyFNYw==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.4.6': resolution: {integrity: sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==} engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.4.7': + resolution: {integrity: sha512-LwQZazFayImv+IOm0S0enoLeUJwmAlhGC5O6YCcLWezyu08dF46GOxPOq35OpBIHkgd7OvNvBStIFwVNyrvoBw==} + engines: {node: '>=18.0.0'} + '@smithy/types@4.14.3': resolution: {integrity: sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==} engines: {node: '>=18.0.0'} + '@smithy/types@4.14.4': + resolution: {integrity: sha512-B2S9+UGm1+/pHkcx3ZoLVX1a+pmSk8rqxRR+ZsNqZaJ5q9FWX9AFGQVM4qG5+OBeQUZVy99HY8HqW8gK/wgXzQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} @@ -2468,75 +2560,75 @@ packages: resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@speed-highlight/core@1.2.15': - resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + '@speed-highlight/core@1.2.17': + resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tailwindcss/node@4.3.0': - resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + '@tailwindcss/node@4.3.1': + resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} - '@tailwindcss/oxide-android-arm64@4.3.0': - resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + '@tailwindcss/oxide-android-arm64@4.3.1': + resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.0': - resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + '@tailwindcss/oxide-darwin-arm64@4.3.1': + resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.0': - resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + '@tailwindcss/oxide-darwin-x64@4.3.1': + resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.0': - resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + '@tailwindcss/oxide-freebsd-x64@4.3.1': + resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': - resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': - resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': - resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': - resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.3.0': - resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.3.0': - resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -2547,54 +2639,54 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': - resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': - resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.0': - resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + '@tailwindcss/oxide@4.3.1': + resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.3.0': - resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + '@tailwindcss/vite@4.3.1': + resolution: {integrity: sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@turbo/darwin-64@2.9.17': - resolution: {integrity: sha512-io5jn5RDeU+9YV78rWhwG++HD/OZ/Lxg1sg93+jDGKQNP3UDxY6RX2dmarbCILhNxNuAM8FH3WgGMY9E96Mf8w==} + '@turbo/darwin-64@2.9.18': + resolution: {integrity: sha512-9f27peFu16ur8c0v9nUFUEyBnbKuuFsUTjHFWfmwGfzySBXbHwzU44QhZon6Mznz0cHsIr3984NQj/bVrnGSRw==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.17': - resolution: {integrity: sha512-83YZTYmN2sxFWf2LTMOwqbOvR3qZMa/TSFwnB6BHVBbIWyoPPe+TAdSTd8KevEx8ml8KkycJ/9A70DFVReyUww==} + '@turbo/darwin-arm64@2.9.18': + resolution: {integrity: sha512-9A6TMRq/Ib+QnbhLlgkhOm+624wO4pzSQ/yQviQfWHOlFvaYxdnIAYmu2H6TS6y7kSVL0DvzNe04NbESTOzFVQ==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.17': - resolution: {integrity: sha512-teKfwJg0zSC+C2ZSOsX3VnAJGVgcN+pgKNmnGWzcpXQ9eIkAQtYP+getrQ2f1Tw/ePudnreQhq8tVP8S73Vy6Q==} + '@turbo/linux-64@2.9.18': + resolution: {integrity: sha512-zCdIDtz69AnbYh913elJRRoF3QY5aa2HNnf+4rAkc7bQ+tWujiDkCNV7stazOUPggaDvhKIf2Z87qHftTeXSkw==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.17': - resolution: {integrity: sha512-mqO36x2CNtJ9CCbEf5xOqH662tVSc1wB0mxh7dopBpgXg0fEifdzwX0IEAUW1WUAaNH986L7iEUUgw3HNqUWgg==} + '@turbo/linux-arm64@2.9.18': + resolution: {integrity: sha512-Va1kXI04naMgYwqv/5Dfa36dTDx8015U7oaQAjrXa45ua9OoFjSV4OmvkML4EmXvUclQHCiBRbY8bvd0jV7eAg==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.17': - resolution: {integrity: sha512-jbyoNePufyMoSSrvVr+/mglcjmya/MOgoIrSHPr67iZ1VFgrlMQXHXtptR2lR48gi+86b1XBvsviJZ9A7zrydg==} + '@turbo/windows-64@2.9.18': + resolution: {integrity: sha512-m0kDhZANxSNz9ck1ybogFscHabriAsp4eDFNrN/1H5WrgTF7b3VlcPZnhuO3v2+E2KnCbeAc+UUT10BZZHdDKw==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.17': - resolution: {integrity: sha512-+ql0wYc99Y2AMvyHCcC/P+xtyV4nz522L+C9HDnyi7ryHXBGM+ZjBP28M7SLBGMDImgpN8sk2szpgbvreMeXVA==} + '@turbo/windows-arm64@2.9.18': + resolution: {integrity: sha512-nUdR8WqoomUys9iIQmG45TMiizJ+5BV8egSeLLZba/AWblyp3fVBcIH1kSE58OtK4g2YzbMJEth6Ttv9w5rqMA==} cpu: [arm64] os: [win32] @@ -2637,8 +2729,8 @@ packages: '@types/node@25.9.1': resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} - '@types/node@25.9.2': - resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + '@types/node@25.9.3': + resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -2655,97 +2747,50 @@ packages: '@types/webxr@0.5.24': resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260603.1': - resolution: {integrity: sha512-BvaaQAHWaHA0nl26DsWTsXsKkCHqUTm7f5FMuNDyCU83Hvo7zHx0vpSTrzL+1KEWeWLUVVGA7U224dk+3yIosQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [darwin] - - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260610.1': - resolution: {integrity: sha512-ClGuxEbvnqDoUYoe8PV+LmXSruS4GYwVgU+l4+S5667ynE3rvZrkQ/tZhS9Z2ew6CI3L16SNn5DFJiOUAI5oWw==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260613.1': + resolution: {integrity: sha512-znTFp/M0SpN91/FM3FrsTycdTZr+bLszmHObm2LaR5sgo1c2wiK4qYsMnrhLeP0vSMD2LuiNg5J21501h7Aj6g==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260603.1': - resolution: {integrity: sha512-a2JJezvJAqWXgTAD1tKdWs7iA/4s3EakLcCYx4rg3ptEbBF6ADWv7A9ySHxT/+CQWYCD0DYIb9du1JUWL85fRw==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260613.1': + resolution: {integrity: sha512-7zXTq3SVw6XCLdm06VRIhMzayG032Ky14xCZ3isnlcm1KD/p4Ev4XL9TN9fZmIV0bVPGMQr4qGS7a2+te8x7lg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260610.1': - resolution: {integrity: sha512-kDMqLXt2tS9zh1AozK0NVh3w2z3HlFFbUMJ4yYY3+yaTxr0WB0WtJzxFKyOiVRIMhhFoeJTauIwqh8aYRYNBdA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [darwin] - - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260603.1': - resolution: {integrity: sha512-MQ0JcRucLdcR6MT14wlrGNz9HaxJrFF8Axmo0IN6e5gSou2UrKKUvvAH1i8zU5Gm7jl8CWCLzzX/qGCi1TsinA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [linux] - - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260610.1': - resolution: {integrity: sha512-Au9raEQUR6eH/1+rURkclshEcgBeGwZR27TDqAhWsM1gLYrgZV0q44pyG8ykPkHFk3hrJlBdI3oAV6+l+HyFBg==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260613.1': + resolution: {integrity: sha512-yYiDEMT4ok7Wd1VtbyB+0YqBdTkU/yzE36edZCbfA/Ljt6L1xZMEn0a7XGlmRtRfwPGHfjtFRFsxrA2nc7jHnw==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260603.1': - resolution: {integrity: sha512-GLsTfJQiGfTN+r1ezlxMcTd5MNYRB/tADD6Y1j1jfLjZsFYSVkNfCnDQA/jwUG6GddBBF+0Um7tBUP16emej9w==} - engines: {node: '>=16.20.0'} - cpu: [arm] - os: [linux] - - '@typescript/native-preview-linux-arm@7.0.0-dev.20260610.1': - resolution: {integrity: sha512-OofDm2YNn9txSsODHliCOp1InHFunzupga78FcA/DKQcG5A93gVeeI3iQYRTje4NWLQxmwETmSyZlvRURB3orA==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260613.1': + resolution: {integrity: sha512-xpA7nXiRE3fwvTUK2ibhkPr7hn6D4i3pB9WjlcX7CSPB2pYGMdHAsL55EAFlR3eNKyNM/geoLfqq4L4PIeOWsQ==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260603.1': - resolution: {integrity: sha512-u6gvCiVGSDagdR2+GI5VJLPxJbGevATJgjZ2QFLKBLWI3re7liTGlPAuaINNDq/r0m9rUPj2rEn0zwqtcLK0nQ==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [linux] - - '@typescript/native-preview-linux-x64@7.0.0-dev.20260610.1': - resolution: {integrity: sha512-31mpOqJHqn+QhFqGEUxw0kUxLVM5V8dTP5CFmn/lgWb2ue4Qvqwmkew4Xb740neiEptcq/cx4Au1iVjEO3MHsQ==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260613.1': + resolution: {integrity: sha512-mZO9hIwXek9VO+N+gs6IKDOf0fPnrQg/SvaOsCDcDwsk7a60yPbacyY7IiXFnxhNgZDLsYqs8j9ojDGl6+Tp5g==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260603.1': - resolution: {integrity: sha512-slxm6HBYs+jk0GpIBC2o03uY5qHgW7wIH+OTjK8JHbd2sUCgYV7P7bfZHUVZYi/cJZcVrnDW6MxXx734TuFn+w==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [win32] - - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260610.1': - resolution: {integrity: sha512-ohktGeyhd+7lwAVvhLKCjdoIWOE405YCCTEckdaXNFfKSjz7sj9Z9yt69IzuevEQrsR8nK23SwWibxDX9tOUlA==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260613.1': + resolution: {integrity: sha512-JtMTHRt7auXTCvvOtcn7aX1gdCPByQl4c5vc8e1d+8PcqtMqeSpH1j0Vp06F9vugTA+YMIzFsJQA91sUK1oHeg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260603.1': - resolution: {integrity: sha512-G7SDZJn2Z9+c1qsAFzI/JL0OsRjJ18diQ39ycWKmJikilZZeL7iS7j933bemWY4ODaLTfxhMRKPMHf9292gpDA==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260613.1': + resolution: {integrity: sha512-aCZaFWHDt5n8B++F2FrTK2pF7muAFfkvi8qkUnC0N0SNBGdVnqlO28IbdjScIfI2JFOvgZtUoYWeATGWdrLXBA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260610.1': - resolution: {integrity: sha512-yq1wzcKX84zCPpcMXpCbjJGpnhwP+4Q5y7xlWo3YCQ/qUz59t7QlnJrC+6XkauddNTgW0rxQfCRnNPc/Qp59Pg==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [win32] - - '@typescript/native-preview@7.0.0-dev.20260603.1': - resolution: {integrity: sha512-CO519Ccw5rji4JIG0DGVMR5owraCeQhm94jM53eRhMdlzz0nAJcAZ63Y6m1u3dUwqGssqlYxh4CcwTFPxTpMYw==} - engines: {node: '>=16.20.0'} - hasBin: true - - '@typescript/native-preview@7.0.0-dev.20260610.1': - resolution: {integrity: sha512-AEeKaUMKVAPGOrSCn436P9YtAQtfZS+T99SYtHMjLtPuSVTcODvoUyyKhuW+7tLXY6NhlX+R+Z0pTRSjAIaq1g==} + '@typescript/native-preview@7.0.0-dev.20260613.1': + resolution: {integrity: sha512-RJ3YrYNoshFGVY862CwZ/WcICvMW0u9XtezI8AAU81I71Cr3/IJoHPUGsGKUr2gVcPfKRQiaI8YTndFzUGvSug==} engines: {node: '>=16.20.0'} hasBin: true @@ -2919,6 +2964,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + anynum@1.0.0: + resolution: {integrity: sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -2940,8 +2988,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - astro@6.4.3: - resolution: {integrity: sha512-heArIk8zLcxuoj1WgBH2zGdAD8zKSU1mEcBvS6hYMEHRPlbtvB+4Y8ri9Z27hzeryvGaFgrH32zjghEfV2y07g==} + astro@6.4.6: + resolution: {integrity: sha512-48OBTBKR9ctbf+DQxpOuxGl8ebfn59zTuNQMBzptmG/Mi/H8IdfMSbJgGuX1I/4U6g9yazG1p4BHlf4+2hWU4Q==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true @@ -3370,8 +3418,8 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - enhanced-resolve@5.22.1: - resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -3427,8 +3475,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -3688,8 +3736,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphql@16.14.1: - resolution: {integrity: sha512-cQOsSMS/IrDz82PVyRDvf/Q1F/bRbBVjJlh+xYOkI1qw2bWRvWGiWc+m2O0d6l4Bt1fyY+8kzJ8JFWGJqNeDBg==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} h3@1.15.11: @@ -3749,8 +3797,8 @@ packages: unstorage: optional: true - hono@4.12.23: - resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + hono@4.12.25: + resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.3: @@ -3778,8 +3826,8 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - human-id@4.1.3: - resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + human-id@4.2.0: + resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true human-signals@8.0.1: @@ -4254,8 +4302,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - miniflare@4.20260601.0: - resolution: {integrity: sha512-56TFiulSEQu43cYxdXgCiA3U3i+Ls0NoXwJXd6DmpNsx8yl/1Il2T3DQ4CMXjR6yfE7CSvC5MuXaqcSAMREjgw==} + miniflare@4.20260611.0: + resolution: {integrity: sha512-i+JwEo8vN96naz1WL3ntFgFyRluBDYL408zwhHKvR2jefJ464KsZ/gCmJAQ5k+oaWeb5Ug+s7yne5AyiAEswjg==} engines: {node: '>=22.0.0'} hasBin: true @@ -4353,8 +4401,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -4567,8 +4616,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.8.3: - resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + prettier@3.8.4: + resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} engines: {node: '>=14'} hasBin: true @@ -4737,13 +4786,13 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.1.0: - resolution: {integrity: sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A==} + rolldown@1.1.1: + resolution: {integrity: sha512-IN750c0p+s3jqJIsFLRZrQazmbAB1kkQDTtQjSt/gbS2ywLhlv4R5Shazer0FZKmuo/BsO3/w2UoYnUjuOZqHg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.61.0: - resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} + rollup@4.62.0: + resolution: {integrity: sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -4777,8 +4826,8 @@ packages: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} - semver@7.8.1: - resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true @@ -4824,8 +4873,8 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} siginfo@2.0.0: @@ -4922,8 +4971,8 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} - strnum@2.3.0: - resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + strnum@2.4.0: + resolution: {integrity: sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==} suf-log@2.5.3: resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} @@ -4941,8 +4990,8 @@ packages: engines: {node: '>=16'} hasBin: true - tailwindcss@4.3.0: - resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + tailwindcss@4.3.1: + resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -5010,8 +5059,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo@2.9.17: - resolution: {integrity: sha512-91Q3KxfHJn7esFu2Ic6j9pkvQqWjncQCOp7r1gCKChRSb/+T/yIjsavAmbGLmFRKAzSjmWW/FMrcknmJ4hEOPA==} + turbo@2.9.18: + resolution: {integrity: sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg==} hasBin: true type-is@2.1.0: @@ -5396,9 +5445,16 @@ packages: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} + vscode-jsonrpc@9.0.0: + resolution: {integrity: sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg==} + engines: {node: '>=14.0.0'} + vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + vscode-languageserver-protocol@3.18.0: + resolution: {integrity: sha512-Zdz+kJ12Iz6tc11xfZyEo501bBATHXrCjmMfnaR3pMnf1CoqZBKIynba3P+/bi9VEdrMbNtAVKYpKhbODvqy+Q==} + vscode-languageserver-textdocument@1.0.12: resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} @@ -5444,17 +5500,17 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260601.1: - resolution: {integrity: sha512-Bg4+HF3B8TW0urAv8chiz25HSQ/aJxMBjgheUzu/nB1NQa+CaKGrUPv+Z3bf0np/WxLHYW1kcseVEtzZVPbX4g==} + workerd@1.20260611.1: + resolution: {integrity: sha512-CS/640T7pIJ2HYX6x2DwKFGbcSckAWN3tgcdq+ptB6SaqjWUhlzIgA/YhPuwIU+/NnMnGpqOFX/hC18Oyge63w==} engines: {node: '>=16'} hasBin: true - wrangler@4.97.0: - resolution: {integrity: sha512-jzW/aNvjerV+4TmwbvwGY6lpcuBk7EFUTonMDNfci45wSmMTj2/OJN+83cc/CeepKdb+6ZjGJw9NRjmcQoxqRg==} + wrangler@4.100.0: + resolution: {integrity: sha512-dSQO7DO+mD6XDzkVWIWBoGLO3yw+lacWSc/KhFvd7pgfpth+kX98qb5SGRHZN8ACCDhhfwzDLXwB6qHsIHhfBg==} engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260601.1 + '@cloudflare/workers-types': ^4.20260611.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -5610,9 +5666,9 @@ snapshots: call-me-maybe: 1.0.2 openapi-types: 12.1.3 - '@astrojs/check@0.9.9(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@6.0.3)': + '@astrojs/check@0.9.9(prettier-plugin-astro@0.14.1)(prettier@3.8.4)(typescript@6.0.3)': dependencies: - '@astrojs/language-server': 2.16.10(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@6.0.3) + '@astrojs/language-server': 2.16.10(prettier-plugin-astro@0.14.1)(prettier@3.8.4)(typescript@6.0.3) chokidar: 4.0.3 kleur: 4.1.5 typescript: 6.0.3 @@ -5636,7 +5692,7 @@ snapshots: smol-toml: 1.6.1 unified: 11.0.5 - '@astrojs/language-server@2.16.10(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@6.0.3)': + '@astrojs/language-server@2.16.10(prettier-plugin-astro@0.14.1)(prettier@3.8.4)(typescript@6.0.3)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 @@ -5650,14 +5706,14 @@ snapshots: volar-service-css: 0.0.70(@volar/language-service@2.4.28) volar-service-emmet: 0.0.70(@volar/language-service@2.4.28) volar-service-html: 0.0.70(@volar/language-service@2.4.28) - volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.8.3) + volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.8.4) volar-service-typescript: 0.0.70(@volar/language-service@2.4.28) volar-service-typescript-twoslash-queries: 0.0.70(@volar/language-service@2.4.28) volar-service-yaml: 0.0.70(@volar/language-service@2.4.28) vscode-html-languageservice: 5.6.2 vscode-uri: 3.1.0 optionalDependencies: - prettier: 3.8.3 + prettier: 3.8.4 prettier-plugin-astro: 0.14.1 transitivePeerDependencies: - typescript @@ -5703,7 +5759,7 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.10 + '@aws-sdk/types': 3.973.12 tslib: 2.8.1 '@aws-crypto/sha256-browser@5.2.0': @@ -5711,15 +5767,15 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.10 - '@aws-sdk/util-locate-window': 3.965.5 + '@aws-sdk/types': 3.973.12 + '@aws-sdk/util-locate-window': 3.965.7 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.10 + '@aws-sdk/types': 3.973.12 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -5728,7 +5784,7 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.10 + '@aws-sdk/types': 3.973.12 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -5749,17 +5805,17 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 - '@aws-sdk/client-cognito-identity@3.1060.0': + '@aws-sdk/client-cognito-identity@3.1068.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.17 - '@aws-sdk/credential-provider-node': 3.972.50 - '@aws-sdk/types': 3.973.10 - '@smithy/core': 3.24.6 - '@smithy/fetch-http-handler': 5.4.6 - '@smithy/node-http-handler': 4.7.6 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.974.20 + '@aws-sdk/credential-provider-node': 3.972.55 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/fetch-http-handler': 5.4.7 + '@smithy/node-http-handler': 4.7.8 + '@smithy/types': 4.14.4 tslib: 2.8.1 '@aws-sdk/core@3.974.17': @@ -5773,12 +5829,23 @@ snapshots: bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-cognito-identity@3.972.40': + '@aws-sdk/core@3.974.20': dependencies: - '@aws-sdk/nested-clients': 3.997.15 - '@aws-sdk/types': 3.973.10 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/types': 3.973.12 + '@aws-sdk/xml-builder': 3.972.29 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.7 + '@smithy/signature-v4': 5.4.7 + '@smithy/types': 4.14.4 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-cognito-identity@3.972.45': + dependencies: + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 tslib: 2.8.1 '@aws-sdk/credential-provider-env@3.972.43': @@ -5789,6 +5856,14 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.46': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.45': dependencies: '@aws-sdk/core': 3.974.17 @@ -5799,6 +5874,16 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.48': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/fetch-http-handler': 5.4.7 + '@smithy/node-http-handler': 4.7.8 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.972.48': dependencies: '@aws-sdk/core': 3.974.17 @@ -5815,6 +5900,22 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.972.53': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/credential-provider-env': 3.972.46 + '@aws-sdk/credential-provider-http': 3.972.48 + '@aws-sdk/credential-provider-login': 3.972.52 + '@aws-sdk/credential-provider-process': 3.972.46 + '@aws-sdk/credential-provider-sso': 3.972.52 + '@aws-sdk/credential-provider-web-identity': 3.972.52 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/credential-provider-imds': 4.3.9 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/credential-provider-login@3.972.47': dependencies: '@aws-sdk/core': 3.974.17 @@ -5824,6 +5925,15 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/credential-provider-login@3.972.52': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.50': dependencies: '@aws-sdk/credential-provider-env': 3.972.43 @@ -5838,6 +5948,20 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.55': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.46 + '@aws-sdk/credential-provider-http': 3.972.48 + '@aws-sdk/credential-provider-ini': 3.972.53 + '@aws-sdk/credential-provider-process': 3.972.46 + '@aws-sdk/credential-provider-sso': 3.972.52 + '@aws-sdk/credential-provider-web-identity': 3.972.52 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/credential-provider-imds': 4.3.9 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.43': dependencies: '@aws-sdk/core': 3.974.17 @@ -5846,6 +5970,14 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.46': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.972.47': dependencies: '@aws-sdk/core': 3.974.17 @@ -5856,6 +5988,16 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.972.52': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/token-providers': 3.1066.0 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/credential-provider-web-identity@3.972.47': dependencies: '@aws-sdk/core': 3.974.17 @@ -5865,24 +6007,33 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 - '@aws-sdk/credential-providers@3.1060.0': + '@aws-sdk/credential-provider-web-identity@3.972.52': dependencies: - '@aws-sdk/client-cognito-identity': 3.1060.0 - '@aws-sdk/core': 3.974.17 - '@aws-sdk/credential-provider-cognito-identity': 3.972.40 - '@aws-sdk/credential-provider-env': 3.972.43 - '@aws-sdk/credential-provider-http': 3.972.45 - '@aws-sdk/credential-provider-ini': 3.972.48 - '@aws-sdk/credential-provider-login': 3.972.47 - '@aws-sdk/credential-provider-node': 3.972.50 - '@aws-sdk/credential-provider-process': 3.972.43 - '@aws-sdk/credential-provider-sso': 3.972.47 - '@aws-sdk/credential-provider-web-identity': 3.972.47 - '@aws-sdk/nested-clients': 3.997.15 - '@aws-sdk/types': 3.973.10 - '@smithy/core': 3.24.6 - '@smithy/credential-provider-imds': 4.3.7 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.974.20 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + + '@aws-sdk/credential-providers@3.1068.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.1068.0 + '@aws-sdk/core': 3.974.20 + '@aws-sdk/credential-provider-cognito-identity': 3.972.45 + '@aws-sdk/credential-provider-env': 3.972.46 + '@aws-sdk/credential-provider-http': 3.972.48 + '@aws-sdk/credential-provider-ini': 3.972.53 + '@aws-sdk/credential-provider-login': 3.972.52 + '@aws-sdk/credential-provider-node': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.46 + '@aws-sdk/credential-provider-sso': 3.972.52 + '@aws-sdk/credential-provider-web-identity': 3.972.52 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/credential-provider-imds': 4.3.9 + '@smithy/types': 4.14.4 tslib: 2.8.1 '@aws-sdk/eventstream-handler-node@3.972.18': @@ -5922,6 +6073,19 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/nested-clients@3.997.20': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.20 + '@aws-sdk/signature-v4-multi-region': 3.996.34 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/fetch-http-handler': 5.4.7 + '@smithy/node-http-handler': 4.7.8 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.31': dependencies: '@aws-sdk/types': 3.973.10 @@ -5929,6 +6093,13 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.34': + dependencies: + '@aws-sdk/types': 3.973.12 + '@smithy/signature-v4': 5.4.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.1048.0': dependencies: '@aws-sdk/core': 3.974.17 @@ -5947,12 +6118,26 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@aws-sdk/token-providers@3.1066.0': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@aws-sdk/types@3.973.10': dependencies: '@smithy/types': 4.14.3 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.965.5': + '@aws-sdk/types@3.973.12': + dependencies: + '@smithy/types': 4.14.4 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.7': dependencies: tslib: 2.8.1 @@ -5962,6 +6147,12 @@ snapshots: fast-xml-parser: 5.7.3 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.29': + dependencies: + '@smithy/types': 4.14.4 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + '@aws/lambda-invoke-store@0.2.4': {} '@babel/helper-string-parser@7.29.7': {} @@ -5979,7 +6170,7 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@capsizecss/unpack@4.0.0': + '@capsizecss/unpack@4.0.1': dependencies: fontkitten: 1.0.3 @@ -5997,7 +6188,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.8.1 + semver: 7.8.4 '@changesets/assemble-release-plan@6.0.10': dependencies: @@ -6006,13 +6197,13 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.8.1 + semver: 7.8.4 '@changesets/changelog-git@0.2.1': dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.31.0(@types/node@25.9.2)': + '@changesets/cli@2.31.0(@types/node@25.9.3)': dependencies: '@changesets/apply-release-plan': 7.1.1 '@changesets/assemble-release-plan': 6.0.10 @@ -6028,7 +6219,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.9.2) + '@inquirer/external-editor': 1.0.3(@types/node@25.9.3) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 enquirer: 2.4.1 @@ -6037,7 +6228,7 @@ snapshots: package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.8.1 + semver: 7.8.4 spawndamnit: 3.0.1 term-size: 2.2.1 transitivePeerDependencies: @@ -6063,7 +6254,7 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.8.1 + semver: 7.8.4 '@changesets/get-release-plan@4.0.16': dependencies: @@ -6123,68 +6314,66 @@ snapshots: dependencies: '@changesets/types': 6.1.0 fs-extra: 7.0.1 - human-id: 4.1.3 + human-id: 4.2.0 prettier: 2.8.8 - '@clack/core@1.4.0': + '@clack/core@1.4.1': dependencies: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clack/prompts@1.5.0': + '@clack/prompts@1.5.1': dependencies: - '@clack/core': 1.4.0 + '@clack/core': 1.4.1 fast-string-width: 3.0.2 fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260601.1)': + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260611.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260601.1 + workerd: 1.20260611.1 - '@cloudflare/unenv-preset@2.7.7(unenv@2.0.0-rc.21)(workerd@1.20260601.1)': + '@cloudflare/unenv-preset@2.7.7(unenv@2.0.0-rc.21)(workerd@1.20260611.1)': dependencies: unenv: 2.0.0-rc.21 optionalDependencies: - workerd: 1.20260601.1 + workerd: 1.20260611.1 '@cloudflare/workerd-darwin-64@1.20260424.1': optional: true - '@cloudflare/workerd-darwin-64@1.20260601.1': + '@cloudflare/workerd-darwin-64@1.20260611.1': optional: true '@cloudflare/workerd-darwin-arm64@1.20260424.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260601.1': + '@cloudflare/workerd-darwin-arm64@1.20260611.1': optional: true '@cloudflare/workerd-linux-64@1.20260424.1': optional: true - '@cloudflare/workerd-linux-64@1.20260601.1': + '@cloudflare/workerd-linux-64@1.20260611.1': optional: true '@cloudflare/workerd-linux-arm64@1.20260424.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260601.1': + '@cloudflare/workerd-linux-arm64@1.20260611.1': optional: true '@cloudflare/workerd-windows-64@1.20260424.1': optional: true - '@cloudflare/workerd-windows-64@1.20260601.1': + '@cloudflare/workerd-windows-64@1.20260611.1': optional: true - '@cloudflare/workers-types@4.20260603.1': {} - - '@cloudflare/workers-types@4.20260610.1': {} + '@cloudflare/workers-types@4.20260613.1': {} '@cspotcode/source-map-support@0.8.1': dependencies: @@ -6289,16 +6478,37 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.0': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -6308,7 +6518,7 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/aix-ppc64@0.28.0': + '@esbuild/aix-ppc64@0.28.1': optional: true '@esbuild/android-arm64@0.25.12': @@ -6320,7 +6530,7 @@ snapshots: '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm64@0.28.0': + '@esbuild/android-arm64@0.28.1': optional: true '@esbuild/android-arm@0.25.12': @@ -6332,7 +6542,7 @@ snapshots: '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-arm@0.28.0': + '@esbuild/android-arm@0.28.1': optional: true '@esbuild/android-x64@0.25.12': @@ -6344,7 +6554,7 @@ snapshots: '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/android-x64@0.28.0': + '@esbuild/android-x64@0.28.1': optional: true '@esbuild/darwin-arm64@0.25.12': @@ -6356,7 +6566,7 @@ snapshots: '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.28.0': + '@esbuild/darwin-arm64@0.28.1': optional: true '@esbuild/darwin-x64@0.25.12': @@ -6368,7 +6578,7 @@ snapshots: '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/darwin-x64@0.28.0': + '@esbuild/darwin-x64@0.28.1': optional: true '@esbuild/freebsd-arm64@0.25.12': @@ -6380,7 +6590,7 @@ snapshots: '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.28.0': + '@esbuild/freebsd-arm64@0.28.1': optional: true '@esbuild/freebsd-x64@0.25.12': @@ -6392,7 +6602,7 @@ snapshots: '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.28.0': + '@esbuild/freebsd-x64@0.28.1': optional: true '@esbuild/linux-arm64@0.25.12': @@ -6404,7 +6614,7 @@ snapshots: '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm64@0.28.0': + '@esbuild/linux-arm64@0.28.1': optional: true '@esbuild/linux-arm@0.25.12': @@ -6416,7 +6626,7 @@ snapshots: '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-arm@0.28.0': + '@esbuild/linux-arm@0.28.1': optional: true '@esbuild/linux-ia32@0.25.12': @@ -6428,7 +6638,7 @@ snapshots: '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-ia32@0.28.0': + '@esbuild/linux-ia32@0.28.1': optional: true '@esbuild/linux-loong64@0.25.12': @@ -6440,7 +6650,7 @@ snapshots: '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-loong64@0.28.0': + '@esbuild/linux-loong64@0.28.1': optional: true '@esbuild/linux-mips64el@0.25.12': @@ -6452,7 +6662,7 @@ snapshots: '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-mips64el@0.28.0': + '@esbuild/linux-mips64el@0.28.1': optional: true '@esbuild/linux-ppc64@0.25.12': @@ -6464,7 +6674,7 @@ snapshots: '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-ppc64@0.28.0': + '@esbuild/linux-ppc64@0.28.1': optional: true '@esbuild/linux-riscv64@0.25.12': @@ -6476,7 +6686,7 @@ snapshots: '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.28.0': + '@esbuild/linux-riscv64@0.28.1': optional: true '@esbuild/linux-s390x@0.25.12': @@ -6488,7 +6698,7 @@ snapshots: '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-s390x@0.28.0': + '@esbuild/linux-s390x@0.28.1': optional: true '@esbuild/linux-x64@0.25.12': @@ -6500,7 +6710,7 @@ snapshots: '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/linux-x64@0.28.0': + '@esbuild/linux-x64@0.28.1': optional: true '@esbuild/netbsd-arm64@0.25.12': @@ -6512,7 +6722,7 @@ snapshots: '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.28.0': + '@esbuild/netbsd-arm64@0.28.1': optional: true '@esbuild/netbsd-x64@0.25.12': @@ -6524,7 +6734,7 @@ snapshots: '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.28.0': + '@esbuild/netbsd-x64@0.28.1': optional: true '@esbuild/openbsd-arm64@0.25.12': @@ -6536,7 +6746,7 @@ snapshots: '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.28.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true '@esbuild/openbsd-x64@0.25.12': @@ -6548,7 +6758,7 @@ snapshots: '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.28.0': + '@esbuild/openbsd-x64@0.28.1': optional: true '@esbuild/openharmony-arm64@0.25.12': @@ -6560,7 +6770,7 @@ snapshots: '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.28.0': + '@esbuild/openharmony-arm64@0.28.1': optional: true '@esbuild/sunos-x64@0.25.12': @@ -6572,7 +6782,7 @@ snapshots: '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/sunos-x64@0.28.0': + '@esbuild/sunos-x64@0.28.1': optional: true '@esbuild/win32-arm64@0.25.12': @@ -6584,7 +6794,7 @@ snapshots: '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-arm64@0.28.0': + '@esbuild/win32-arm64@0.28.1': optional: true '@esbuild/win32-ia32@0.25.12': @@ -6596,7 +6806,7 @@ snapshots: '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-ia32@0.28.0': + '@esbuild/win32-ia32@0.28.1': optional: true '@esbuild/win32-x64@0.25.12': @@ -6608,7 +6818,7 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@esbuild/win32-x64@0.28.0': + '@esbuild/win32-x64@0.28.1': optional: true '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': @@ -6624,21 +6834,21 @@ snapshots: - supports-color - utf-8-validate - '@hono/mcp@0.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(hono-rate-limiter@0.5.3(hono@4.12.23)(unstorage@1.17.5(aws4fetch@1.0.20)))(hono@4.12.23)(zod@4.4.3)': + '@hono/mcp@0.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(hono-rate-limiter@0.5.3(hono@4.12.25)(unstorage@1.17.5(aws4fetch@1.0.20)))(hono@4.12.25)(zod@4.4.3)': dependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - hono: 4.12.23 - hono-rate-limiter: 0.5.3(hono@4.12.23)(unstorage@1.17.5(aws4fetch@1.0.20)) + hono: 4.12.25 + hono-rate-limiter: 0.5.3(hono@4.12.25)(unstorage@1.17.5(aws4fetch@1.0.20)) pkce-challenge: 5.0.1 zod: 4.4.3 - '@hono/node-server@1.19.14(hono@4.12.23)': + '@hono/node-server@1.19.14(hono@4.12.25)': dependencies: - hono: 4.12.23 + hono: 4.12.25 - '@hono/node-server@2.0.4(hono@4.12.23)': + '@hono/node-server@2.0.4(hono@4.12.25)': dependencies: - hono: 4.12.23 + hono: 4.12.25 '@hugeicons/core-free-icons@4.2.0': {} @@ -6728,7 +6938,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -6740,12 +6950,12 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/external-editor@1.0.3(@types/node@25.9.2)': + '@inquirer/external-editor@1.0.3(@types/node@25.9.3)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 '@isaacs/cliui@8.0.2': dependencies: @@ -6869,7 +7079,7 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.23) + '@hono/node-server': 1.19.14(hono@4.12.25) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -6879,7 +7089,7 @@ snapshots: eventsource-parser: 3.1.0 express: 5.2.1 express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.23 + hono: 4.12.25 jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -6907,14 +7117,21 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': optional: true - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.2 optional: true - '@nodable/entities@2.1.1': {} + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@nodable/entities@2.2.0': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -7010,7 +7227,7 @@ snapshots: '@oxc-project/types@0.133.0': {} - '@oxc-project/types@0.134.0': {} + '@oxc-project/types@0.135.0': {} '@oxfmt/binding-android-arm-eabi@0.54.0': optional: true @@ -7166,184 +7383,184 @@ snapshots: '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rolldown/binding-android-arm64@1.1.0': + '@rolldown/binding-android-arm64@1.1.1': optional: true '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-arm64@1.1.0': + '@rolldown/binding-darwin-arm64@1.1.1': optional: true '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rolldown/binding-darwin-x64@1.1.0': + '@rolldown/binding-darwin-x64@1.1.1': optional: true '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rolldown/binding-freebsd-x64@1.1.0': + '@rolldown/binding-freebsd-x64@1.1.1': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.0': + '@rolldown/binding-linux-arm-gnueabihf@1.1.1': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.0': + '@rolldown/binding-linux-arm64-gnu@1.1.1': optional: true '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.0': + '@rolldown/binding-linux-arm64-musl@1.1.1': optional: true '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.0': + '@rolldown/binding-linux-ppc64-gnu@1.1.1': optional: true '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.0': + '@rolldown/binding-linux-s390x-gnu@1.1.1': optional: true '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.0': + '@rolldown/binding-linux-x64-gnu@1.1.1': optional: true '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rolldown/binding-linux-x64-musl@1.1.0': + '@rolldown/binding-linux-x64-musl@1.1.1': optional: true '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rolldown/binding-openharmony-arm64@1.1.0': + '@rolldown/binding-openharmony-arm64@1.1.1': optional: true '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-wasm32-wasi@1.1.0': + '@rolldown/binding-wasm32-wasi@1.1.1': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) optional: true '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.0': + '@rolldown/binding-win32-arm64-msvc@1.1.1': optional: true '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.0': + '@rolldown/binding-win32-x64-msvc@1.1.1': optional: true '@rolldown/pluginutils@1.0.1': {} - '@rollup/pluginutils@5.4.0(rollup@4.61.0)': + '@rollup/pluginutils@5.4.0(rollup@4.62.0)': dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 picomatch: 4.0.4 optionalDependencies: - rollup: 4.61.0 + rollup: 4.62.0 - '@rollup/rollup-android-arm-eabi@4.61.0': + '@rollup/rollup-android-arm-eabi@4.62.0': optional: true - '@rollup/rollup-android-arm64@4.61.0': + '@rollup/rollup-android-arm64@4.62.0': optional: true - '@rollup/rollup-darwin-arm64@4.61.0': + '@rollup/rollup-darwin-arm64@4.62.0': optional: true - '@rollup/rollup-darwin-x64@4.61.0': + '@rollup/rollup-darwin-x64@4.62.0': optional: true - '@rollup/rollup-freebsd-arm64@4.61.0': + '@rollup/rollup-freebsd-arm64@4.62.0': optional: true - '@rollup/rollup-freebsd-x64@4.61.0': + '@rollup/rollup-freebsd-x64@4.62.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + '@rollup/rollup-linux-arm-gnueabihf@4.62.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.61.0': + '@rollup/rollup-linux-arm-musleabihf@4.62.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.61.0': + '@rollup/rollup-linux-arm64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.61.0': + '@rollup/rollup-linux-arm64-musl@4.62.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.61.0': + '@rollup/rollup-linux-loong64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.61.0': + '@rollup/rollup-linux-loong64-musl@4.62.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.61.0': + '@rollup/rollup-linux-ppc64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.61.0': + '@rollup/rollup-linux-ppc64-musl@4.62.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.61.0': + '@rollup/rollup-linux-riscv64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.61.0': + '@rollup/rollup-linux-riscv64-musl@4.62.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.61.0': + '@rollup/rollup-linux-s390x-gnu@4.62.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.61.0': + '@rollup/rollup-linux-x64-gnu@4.62.0': optional: true - '@rollup/rollup-linux-x64-musl@4.61.0': + '@rollup/rollup-linux-x64-musl@4.62.0': optional: true - '@rollup/rollup-openbsd-x64@4.61.0': + '@rollup/rollup-openbsd-x64@4.62.0': optional: true - '@rollup/rollup-openharmony-arm64@4.61.0': + '@rollup/rollup-openharmony-arm64@4.62.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.61.0': + '@rollup/rollup-win32-arm64-msvc@4.62.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.61.0': + '@rollup/rollup-win32-ia32-msvc@4.62.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.61.0': + '@rollup/rollup-win32-x64-gnu@4.62.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.61.0': + '@rollup/rollup-win32-x64-msvc@4.62.0': optional: true '@sec-ant/readable-stream@0.4.1': {} @@ -7400,12 +7617,24 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@smithy/core@3.24.7': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.3.7': dependencies: '@smithy/core': 3.24.6 '@smithy/types': 4.14.3 tslib: 2.8.1 + '@smithy/credential-provider-imds@4.3.9': + dependencies: + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@smithy/fetch-http-handler@5.4.5': dependencies: '@smithy/core': 3.24.6 @@ -7418,13 +7647,19 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@smithy/fetch-http-handler@5.4.7': + dependencies: + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 - '@smithy/node-config-provider@4.4.6': + '@smithy/node-config-provider@4.4.7': dependencies: - '@smithy/core': 3.24.6 + '@smithy/core': 3.24.7 tslib: 2.8.1 '@smithy/node-http-handler@4.7.3': @@ -7439,16 +7674,32 @@ snapshots: '@smithy/types': 4.14.3 tslib: 2.8.1 + '@smithy/node-http-handler@4.7.8': + dependencies: + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@smithy/signature-v4@5.4.6': dependencies: '@smithy/core': 3.24.6 '@smithy/types': 4.14.3 tslib: 2.8.1 + '@smithy/signature-v4@5.4.7': + dependencies: + '@smithy/core': 3.24.7 + '@smithy/types': 4.14.4 + tslib: 2.8.1 + '@smithy/types@4.14.3': dependencies: tslib: 2.8.1 + '@smithy/types@4.14.4': + dependencies: + tslib: 2.8.1 + '@smithy/util-buffer-from@2.2.0': dependencies: '@smithy/is-array-buffer': 2.2.0 @@ -7459,94 +7710,94 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@speed-highlight/core@1.2.15': {} + '@speed-highlight/core@1.2.17': {} '@standard-schema/spec@1.1.0': {} - '@tailwindcss/node@4.3.0': + '@tailwindcss/node@4.3.1': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.22.1 + enhanced-resolve: 5.21.6 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.0 + tailwindcss: 4.3.1 - '@tailwindcss/oxide-android-arm64@4.3.0': + '@tailwindcss/oxide-android-arm64@4.3.1': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.0': + '@tailwindcss/oxide-darwin-arm64@4.3.1': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.0': + '@tailwindcss/oxide-darwin-x64@4.3.1': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.0': + '@tailwindcss/oxide-freebsd-x64@4.3.1': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.0': + '@tailwindcss/oxide-linux-x64-musl@4.3.1': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.0': + '@tailwindcss/oxide-wasm32-wasi@4.3.1': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': optional: true - '@tailwindcss/oxide@4.3.0': + '@tailwindcss/oxide@4.3.1': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-x64': 4.3.0 - '@tailwindcss/oxide-freebsd-x64': 4.3.0 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-x64-musl': 4.3.0 - '@tailwindcss/oxide-wasm32-wasi': 4.3.0 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + '@tailwindcss/oxide-android-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-x64': 4.3.1 + '@tailwindcss/oxide-freebsd-x64': 4.3.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-x64-musl': 4.3.1 + '@tailwindcss/oxide-wasm32-wasi': 4.3.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 - '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.1(vite@7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: - '@tailwindcss/node': 4.3.0 - '@tailwindcss/oxide': 4.3.0 - tailwindcss: 4.3.0 - vite: 7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) + '@tailwindcss/node': 4.3.1 + '@tailwindcss/oxide': 4.3.1 + tailwindcss: 4.3.1 + vite: 7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) - '@turbo/darwin-64@2.9.17': + '@turbo/darwin-64@2.9.18': optional: true - '@turbo/darwin-arm64@2.9.17': + '@turbo/darwin-arm64@2.9.18': optional: true - '@turbo/linux-64@2.9.17': + '@turbo/linux-64@2.9.18': optional: true - '@turbo/linux-arm64@2.9.17': + '@turbo/linux-arm64@2.9.18': optional: true - '@turbo/windows-64@2.9.17': + '@turbo/windows-64@2.9.18': optional: true - '@turbo/windows-arm64@2.9.17': + '@turbo/windows-arm64@2.9.18': optional: true '@tweenjs/tween.js@23.1.3': {} @@ -7591,7 +7842,7 @@ snapshots: dependencies: undici-types: 7.24.6 - '@types/node@25.9.2': + '@types/node@25.9.3': dependencies: undici-types: 7.24.6 @@ -7612,67 +7863,36 @@ snapshots: '@types/webxr@0.5.24': {} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260603.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260613.1': optional: true - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260610.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260613.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260603.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260613.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260610.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260613.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260603.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260613.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260610.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260613.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260603.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260613.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260610.1': - optional: true - - '@typescript/native-preview-linux-x64@7.0.0-dev.20260603.1': - optional: true - - '@typescript/native-preview-linux-x64@7.0.0-dev.20260610.1': - optional: true - - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260603.1': - optional: true - - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260610.1': - optional: true - - '@typescript/native-preview-win32-x64@7.0.0-dev.20260603.1': - optional: true - - '@typescript/native-preview-win32-x64@7.0.0-dev.20260610.1': - optional: true - - '@typescript/native-preview@7.0.0-dev.20260603.1': - optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260603.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260603.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260603.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260603.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260603.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260603.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260603.1 - - '@typescript/native-preview@7.0.0-dev.20260610.1': + '@typescript/native-preview@7.0.0-dev.20260613.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260610.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260610.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260610.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260610.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260610.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260610.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260610.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260613.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260613.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260613.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260613.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260613.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260613.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260613.1 '@ungap/structured-clone@1.3.1': {} @@ -7685,21 +7905,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) - - '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.8 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) '@vitest/pretty-format@4.1.8': dependencies: @@ -7746,14 +7958,14 @@ snapshots: path-browserify: 1.0.1 request-light: 0.7.0 vscode-languageserver: 9.0.1 - vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-protocol: 3.18.0 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 '@volar/language-service@2.4.28': dependencies: '@volar/language-core': 2.4.28 - vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-protocol: 3.18.0 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 @@ -7797,17 +8009,17 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy@0.93.12(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(workerd@1.20260601.1): + alchemy@0.93.12(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(workerd@1.20260611.1): dependencies: - '@aws-sdk/credential-providers': 3.1060.0 - '@cloudflare/unenv-preset': 2.7.7(unenv@2.0.0-rc.21)(workerd@1.20260601.1) - '@cloudflare/workers-types': 4.20260603.1 + '@aws-sdk/credential-providers': 3.1068.0 + '@cloudflare/unenv-preset': 2.7.7(unenv@2.0.0-rc.21)(workerd@1.20260611.1) + '@cloudflare/workers-types': 4.20260613.1 '@iarna/toml': 2.2.5 '@octokit/rest': 21.1.1 - '@smithy/node-config-provider': 4.4.6 - '@smithy/types': 4.14.3 + '@smithy/node-config-provider': 4.4.7 + '@smithy/types': 4.14.4 aws4fetch: 1.0.20 - drizzle-orm: 0.45.2(@cloudflare/workers-types@4.20260603.1) + drizzle-orm: 0.45.2(@cloudflare/workers-types@4.20260613.1) env-paths: 3.0.0 esbuild: 0.25.12 execa: 9.6.1 @@ -7826,11 +8038,11 @@ snapshots: proper-lockfile: 4.1.2 signal-exit: 4.1.0 unenv: 2.0.0-rc.21 - wrangler: 4.97.0(@cloudflare/workers-types@4.20260603.1) + wrangler: 4.100.0(@cloudflare/workers-types@4.20260613.1) ws: 8.21.0 yaml: 2.9.0 optionalDependencies: - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) transitivePeerDependencies: - '@aws-sdk/client-rds-data' - '@electric-sql/pglite' @@ -7884,6 +8096,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 + anynum@1.0.0: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -7898,16 +8112,16 @@ snapshots: assertion-error@2.0.1: {} - astro@6.4.3(@types/node@25.9.2)(aws4fetch@1.0.20)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.0)(tsx@4.22.4)(yaml@2.9.0): + astro@6.4.6(@types/node@25.9.3)(aws4fetch@1.0.20)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.62.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: '@astrojs/compiler': 4.0.0 '@astrojs/internal-helpers': 0.10.0 '@astrojs/markdown-remark': 7.2.0 '@astrojs/telemetry': 3.3.2 - '@capsizecss/unpack': 4.0.0 - '@clack/prompts': 1.5.0 + '@capsizecss/unpack': 4.0.1 + '@clack/prompts': 1.5.1 '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.4.0(rollup@4.61.0) + '@rollup/pluginutils': 5.4.0(rollup@4.62.0) aria-query: 5.3.2 axobject-query: 4.1.0 ci-info: 4.4.0 @@ -7931,14 +8145,14 @@ snapshots: magicast: 0.5.3 mrmime: 2.0.1 neotraverse: 0.6.18 - obug: 2.1.1 + obug: 2.1.3 p-limit: 7.3.0 p-queue: 9.3.0 package-manager-detector: 1.6.0 piccolore: 0.1.3 picomatch: 4.0.4 rehype: 13.0.2 - semver: 7.8.1 + semver: 7.8.4 shiki: 4.2.0 smol-toml: 1.6.1 svgo: 4.0.1 @@ -7950,8 +8164,8 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.5(aws4fetch@1.0.20) vfile: 6.0.3 - vite: 7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) - vitefu: 1.1.3(vite@7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + vite: 7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) + vitefu: 1.1.3(vite@7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 @@ -8243,9 +8457,9 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260603.1): + drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260613.1): optionalDependencies: - '@cloudflare/workers-types': 4.20260603.1 + '@cloudflare/workers-types': 4.20260613.1 dset@3.1.4: {} @@ -8289,7 +8503,7 @@ snapshots: encodeurl@2.0.0: {} - enhanced-resolve@5.22.1: + enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -8406,34 +8620,34 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.0: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -8557,17 +8771,17 @@ snapshots: fast-xml-parser@5.7.3: dependencies: - '@nodable/entities': 2.1.1 + '@nodable/entities': 2.2.0 fast-xml-builder: 1.2.0 path-expression-matcher: 1.5.0 - strnum: 2.3.0 + strnum: 2.4.0 fast-xml-parser@5.8.0: dependencies: - '@nodable/entities': 2.1.1 + '@nodable/entities': 2.2.0 fast-xml-builder: 1.2.0 path-expression-matcher: 1.5.0 - strnum: 2.3.0 + strnum: 2.4.0 xml-naming: 0.1.0 fastq@1.20.1: @@ -8751,7 +8965,7 @@ snapshots: graceful-fs@4.2.11: {} - graphql@16.14.1: {} + graphql@16.14.2: {} h3@1.15.11: dependencies: @@ -8862,13 +9076,13 @@ snapshots: highlight.js@10.7.3: {} - hono-rate-limiter@0.5.3(hono@4.12.23)(unstorage@1.17.5(aws4fetch@1.0.20)): + hono-rate-limiter@0.5.3(hono@4.12.25)(unstorage@1.17.5(aws4fetch@1.0.20)): dependencies: - hono: 4.12.23 + hono: 4.12.25 optionalDependencies: unstorage: 1.17.5(aws4fetch@1.0.20) - hono@4.12.23: {} + hono@4.12.25: {} hosted-git-info@9.0.3: dependencies: @@ -8902,7 +9116,7 @@ snapshots: transitivePeerDependencies: - supports-color - human-id@4.1.3: {} + human-id@4.2.0: {} human-signals@8.0.1: {} @@ -9502,12 +9716,12 @@ snapshots: - bufferutil - utf-8-validate - miniflare@4.20260601.0: + miniflare@4.20260611.0: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 undici: 7.24.8 - workerd: 1.20260601.1 + workerd: 1.20260611.1 ws: 8.20.1 youch: 4.1.0-beta.10 transitivePeerDependencies: @@ -9558,7 +9772,7 @@ snapshots: neverthrow@8.2.0: optionalDependencies: - '@rollup/rollup-linux-x64-gnu': 4.61.0 + '@rollup/rollup-linux-x64-gnu': 4.62.0 nlcst-to-string@4.0.0: dependencies: @@ -9596,7 +9810,7 @@ snapshots: object-inspect@1.13.4: {} - obug@2.1.1: {} + obug@2.1.3: {} ofetch@1.5.1: dependencies: @@ -9798,12 +10012,12 @@ snapshots: prettier-plugin-astro@0.14.1: dependencies: '@astrojs/compiler': 2.13.1 - prettier: 3.8.3 + prettier: 3.8.4 sass-formatter: 0.7.9 prettier@2.8.8: {} - prettier@3.8.3: {} + prettier@3.8.4: {} pretty-ms@9.3.0: dependencies: @@ -9845,7 +10059,7 @@ snapshots: qs@6.15.2: dependencies: - side-channel: 1.1.0 + side-channel: 1.1.1 quansync@0.2.11: {} @@ -10042,56 +10256,56 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.3 '@rolldown/binding-win32-x64-msvc': 1.0.3 - rolldown@1.1.0: + rolldown@1.1.1: dependencies: - '@oxc-project/types': 0.134.0 + '@oxc-project/types': 0.135.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.0 - '@rolldown/binding-darwin-arm64': 1.1.0 - '@rolldown/binding-darwin-x64': 1.1.0 - '@rolldown/binding-freebsd-x64': 1.1.0 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.0 - '@rolldown/binding-linux-arm64-gnu': 1.1.0 - '@rolldown/binding-linux-arm64-musl': 1.1.0 - '@rolldown/binding-linux-ppc64-gnu': 1.1.0 - '@rolldown/binding-linux-s390x-gnu': 1.1.0 - '@rolldown/binding-linux-x64-gnu': 1.1.0 - '@rolldown/binding-linux-x64-musl': 1.1.0 - '@rolldown/binding-openharmony-arm64': 1.1.0 - '@rolldown/binding-wasm32-wasi': 1.1.0 - '@rolldown/binding-win32-arm64-msvc': 1.1.0 - '@rolldown/binding-win32-x64-msvc': 1.1.0 - - rollup@4.61.0: + '@rolldown/binding-android-arm64': 1.1.1 + '@rolldown/binding-darwin-arm64': 1.1.1 + '@rolldown/binding-darwin-x64': 1.1.1 + '@rolldown/binding-freebsd-x64': 1.1.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.1 + '@rolldown/binding-linux-arm64-gnu': 1.1.1 + '@rolldown/binding-linux-arm64-musl': 1.1.1 + '@rolldown/binding-linux-ppc64-gnu': 1.1.1 + '@rolldown/binding-linux-s390x-gnu': 1.1.1 + '@rolldown/binding-linux-x64-gnu': 1.1.1 + '@rolldown/binding-linux-x64-musl': 1.1.1 + '@rolldown/binding-openharmony-arm64': 1.1.1 + '@rolldown/binding-wasm32-wasi': 1.1.1 + '@rolldown/binding-win32-arm64-msvc': 1.1.1 + '@rolldown/binding-win32-x64-msvc': 1.1.1 + + rollup@4.62.0: dependencies: '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.61.0 - '@rollup/rollup-android-arm64': 4.61.0 - '@rollup/rollup-darwin-arm64': 4.61.0 - '@rollup/rollup-darwin-x64': 4.61.0 - '@rollup/rollup-freebsd-arm64': 4.61.0 - '@rollup/rollup-freebsd-x64': 4.61.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.61.0 - '@rollup/rollup-linux-arm-musleabihf': 4.61.0 - '@rollup/rollup-linux-arm64-gnu': 4.61.0 - '@rollup/rollup-linux-arm64-musl': 4.61.0 - '@rollup/rollup-linux-loong64-gnu': 4.61.0 - '@rollup/rollup-linux-loong64-musl': 4.61.0 - '@rollup/rollup-linux-ppc64-gnu': 4.61.0 - '@rollup/rollup-linux-ppc64-musl': 4.61.0 - '@rollup/rollup-linux-riscv64-gnu': 4.61.0 - '@rollup/rollup-linux-riscv64-musl': 4.61.0 - '@rollup/rollup-linux-s390x-gnu': 4.61.0 - '@rollup/rollup-linux-x64-gnu': 4.61.0 - '@rollup/rollup-linux-x64-musl': 4.61.0 - '@rollup/rollup-openbsd-x64': 4.61.0 - '@rollup/rollup-openharmony-arm64': 4.61.0 - '@rollup/rollup-win32-arm64-msvc': 4.61.0 - '@rollup/rollup-win32-ia32-msvc': 4.61.0 - '@rollup/rollup-win32-x64-gnu': 4.61.0 - '@rollup/rollup-win32-x64-msvc': 4.61.0 + '@rollup/rollup-android-arm-eabi': 4.62.0 + '@rollup/rollup-android-arm64': 4.62.0 + '@rollup/rollup-darwin-arm64': 4.62.0 + '@rollup/rollup-darwin-x64': 4.62.0 + '@rollup/rollup-freebsd-arm64': 4.62.0 + '@rollup/rollup-freebsd-x64': 4.62.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.0 + '@rollup/rollup-linux-arm-musleabihf': 4.62.0 + '@rollup/rollup-linux-arm64-gnu': 4.62.0 + '@rollup/rollup-linux-arm64-musl': 4.62.0 + '@rollup/rollup-linux-loong64-gnu': 4.62.0 + '@rollup/rollup-linux-loong64-musl': 4.62.0 + '@rollup/rollup-linux-ppc64-gnu': 4.62.0 + '@rollup/rollup-linux-ppc64-musl': 4.62.0 + '@rollup/rollup-linux-riscv64-gnu': 4.62.0 + '@rollup/rollup-linux-riscv64-musl': 4.62.0 + '@rollup/rollup-linux-s390x-gnu': 4.62.0 + '@rollup/rollup-linux-x64-gnu': 4.62.0 + '@rollup/rollup-linux-x64-musl': 4.62.0 + '@rollup/rollup-openbsd-x64': 4.62.0 + '@rollup/rollup-openharmony-arm64': 4.62.0 + '@rollup/rollup-win32-arm64-msvc': 4.62.0 + '@rollup/rollup-win32-ia32-msvc': 4.62.0 + '@rollup/rollup-win32-x64-gnu': 4.62.0 + '@rollup/rollup-win32-x64-msvc': 4.62.0 fsevents: 2.3.3 router@2.2.0: @@ -10124,7 +10338,7 @@ snapshots: sax@1.6.0: {} - semver@7.8.1: {} + semver@7.8.4: {} send@1.2.1: dependencies: @@ -10159,7 +10373,7 @@ snapshots: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.1 + semver: 7.8.4 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -10223,7 +10437,7 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: + side-channel@1.1.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -10316,7 +10530,9 @@ snapshots: strip-final-newline@4.0.0: {} - strnum@2.3.0: {} + strnum@2.4.0: + dependencies: + anynum: 1.0.0 suf-log@2.5.3: dependencies: @@ -10338,7 +10554,7 @@ snapshots: picocolors: 1.1.1 sax: 1.6.0 - tailwindcss@4.3.0: {} + tailwindcss@4.3.1: {} tapable@2.3.3: {} @@ -10381,18 +10597,18 @@ snapshots: tsx@4.22.4: dependencies: - esbuild: 0.28.0 + esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 - turbo@2.9.17: + turbo@2.9.18: optionalDependencies: - '@turbo/darwin-64': 2.9.17 - '@turbo/darwin-arm64': 2.9.17 - '@turbo/linux-64': 2.9.17 - '@turbo/linux-arm64': 2.9.17 - '@turbo/windows-64': 2.9.17 - '@turbo/windows-arm64': 2.9.17 + '@turbo/darwin-64': 2.9.18 + '@turbo/darwin-arm64': 2.9.18 + '@turbo/linux-64': 2.9.18 + '@turbo/linux-arm64': 2.9.18 + '@turbo/windows-64': 2.9.18 + '@turbo/windows-arm64': 2.9.18 type-is@2.1.0: dependencies: @@ -10406,7 +10622,7 @@ snapshots: typescript-auto-import-cache@0.3.6: dependencies: - semver: 7.8.1 + semver: 7.8.4 typescript@6.0.3: {} @@ -10539,38 +10755,23 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0): + vite@7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.15 - rollup: 4.61.0 + rollup: 4.62.0 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 tsx: 4.22.4 yaml: 2.9.0 - vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 25.9.1 - esbuild: 0.28.0 - fsevents: 2.3.3 - jiti: 2.7.0 - tsx: 4.22.4 - yaml: 2.9.0 - - vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): + vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -10578,21 +10779,21 @@ snapshots: rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.2 - esbuild: 0.28.0 + '@types/node': 25.9.3 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 tsx: 4.22.4 yaml: 2.9.0 - vitefu@1.1.3(vite@7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)): + vitefu@1.1.3(vite@7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)): optionalDependencies: - vite: 7.3.5(@types/node@25.9.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 7.3.5(@types/node@25.9.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) - vitest@4.1.8(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.8(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -10601,7 +10802,7 @@ snapshots: es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.3 pathe: 2.0.3 picomatch: 4.0.4 std-env: 4.1.0 @@ -10609,37 +10810,10 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.1 - transitivePeerDependencies: - - msw - - vitest@4.1.8(@types/node@25.9.2)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.8 - '@vitest/runner': 4.1.8 - '@vitest/snapshot': 4.1.8 - '@vitest/spy': 4.1.8 - '@vitest/utils': 4.1.8 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 25.9.2 + '@types/node': 25.9.3 transitivePeerDependencies: - msw @@ -10668,12 +10842,12 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.8.3): + volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.8.4): dependencies: vscode-uri: 3.1.0 optionalDependencies: '@volar/language-service': 2.4.28 - prettier: 3.8.3 + prettier: 3.8.4 volar-service-typescript-twoslash-queries@0.0.70(@volar/language-service@2.4.28): dependencies: @@ -10684,7 +10858,7 @@ snapshots: volar-service-typescript@0.0.70(@volar/language-service@2.4.28): dependencies: path-browserify: 1.0.1 - semver: 7.8.1 + semver: 7.8.4 typescript-auto-import-cache: 0.3.6 vscode-languageserver-textdocument: 1.0.12 vscode-nls: 5.2.0 @@ -10723,11 +10897,18 @@ snapshots: vscode-jsonrpc@8.2.0: {} + vscode-jsonrpc@9.0.0: {} + vscode-languageserver-protocol@3.17.5: dependencies: vscode-jsonrpc: 8.2.0 vscode-languageserver-types: 3.17.5 + vscode-languageserver-protocol@3.18.0: + dependencies: + vscode-jsonrpc: 9.0.0 + vscode-languageserver-types: 3.18.0 + vscode-languageserver-textdocument@1.0.12: {} vscode-languageserver-types@3.17.5: {} @@ -10765,26 +10946,26 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260424.1 '@cloudflare/workerd-windows-64': 1.20260424.1 - workerd@1.20260601.1: + workerd@1.20260611.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260601.1 - '@cloudflare/workerd-darwin-arm64': 1.20260601.1 - '@cloudflare/workerd-linux-64': 1.20260601.1 - '@cloudflare/workerd-linux-arm64': 1.20260601.1 - '@cloudflare/workerd-windows-64': 1.20260601.1 + '@cloudflare/workerd-darwin-64': 1.20260611.1 + '@cloudflare/workerd-darwin-arm64': 1.20260611.1 + '@cloudflare/workerd-linux-64': 1.20260611.1 + '@cloudflare/workerd-linux-arm64': 1.20260611.1 + '@cloudflare/workerd-windows-64': 1.20260611.1 - wrangler@4.97.0(@cloudflare/workers-types@4.20260603.1): + wrangler@4.100.0(@cloudflare/workers-types@4.20260613.1): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260601.1) + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260611.1) blake3-wasm: 2.1.5 esbuild: 0.27.3 - miniflare: 4.20260601.0 + miniflare: 4.20260611.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260601.1 + workerd: 1.20260611.1 optionalDependencies: - '@cloudflare/workers-types': 4.20260603.1 + '@cloudflare/workers-types': 4.20260613.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil @@ -10837,7 +11018,7 @@ snapshots: '@vscode/l10n': 0.0.18 ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) - prettier: 3.8.3 + prettier: 3.8.4 request-light: 0.5.8 vscode-json-languageservice: 4.1.8 vscode-languageserver: 9.0.1 @@ -10877,7 +11058,7 @@ snapshots: dependencies: '@poppinss/colors': 4.1.6 '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.15 + '@speed-highlight/core': 1.2.17 cookie: 1.1.1 youch-core: 0.3.3 diff --git a/schemas/caplet.schema.json b/schemas/caplet.schema.json index 7f21f86a..aab59d71 100644 --- a/schemas/caplet.schema.json +++ b/schemas/caplet.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json", + "$id": "https://caplets.dev/caplet-frontmatter.schema.json", "title": "Caplet file frontmatter", "description": "YAML frontmatter schema for a Markdown Caplet file.", "type": "object", diff --git a/schemas/caplets-config.schema.json b/schemas/caplets-config.schema.json index 6fb9c025..9cd1369a 100644 --- a/schemas/caplets-config.schema.json +++ b/schemas/caplets-config.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplets-config.schema.json", + "$id": "https://caplets.dev/config.schema.json", "title": "Caplets config", "description": "Configuration file for the Caplets progressive MCP disclosure gateway.", "type": "object", diff --git a/scripts/generate-config-schema.ts b/scripts/generate-config-schema.ts index 91466a99..2b5231d4 100644 --- a/scripts/generate-config-schema.ts +++ b/scripts/generate-config-schema.ts @@ -8,25 +8,41 @@ import { configJsonSchema } from "../packages/core/src/config"; const repoRoot = findRepoRoot(dirname(fileURLToPath(import.meta.url))); const schemas = [ - { path: join(repoRoot, "schemas/caplets-config.schema.json"), schema: configJsonSchema() }, - { path: join(repoRoot, "schemas/caplet.schema.json"), schema: capletJsonSchema() }, + { + paths: [ + join(repoRoot, "schemas/caplets-config.schema.json"), + join(repoRoot, "apps/landing/public/config.schema.json"), + ], + schema: configJsonSchema(), + }, + { + paths: [ + join(repoRoot, "schemas/caplet.schema.json"), + join(repoRoot, "apps/landing/public/caplet-frontmatter.schema.json"), + ], + schema: capletJsonSchema(), + }, ]; if (process.argv.includes("--check")) { for (const entry of schemas) { const next = formatJson(`${JSON.stringify(entry.schema, null, 2)}\n`); - const current = existsSync(entry.path) ? readFileSync(entry.path, "utf8") : ""; - if (current !== next) { - console.error(`${entry.path} is out of date. Run pnpm schema:generate.`); - process.exitCode = 1; + for (const path of entry.paths) { + const current = existsSync(path) ? readFileSync(path, "utf8") : ""; + if (current !== next) { + console.error(`${path} is out of date. Run pnpm schema:generate.`); + process.exitCode = 1; + } } } } else { for (const entry of schemas) { const next = formatJson(`${JSON.stringify(entry.schema, null, 2)}\n`); - mkdirSync(dirname(entry.path), { recursive: true }); - writeFileSync(entry.path, next); - console.log(`Generated ${entry.path}`); + for (const path of entry.paths) { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, next); + console.log(`Generated ${path}`); + } } }