diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 850afc7..3397a0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Bootstrap run: ./scripts/bootstrap @@ -46,7 +46,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Bootstrap run: ./scripts/bootstrap @@ -68,6 +68,15 @@ jobs: AUTH: ${{ steps.github-oidc.outputs.github_token }} SHA: ${{ github.sha }} run: ./scripts/utils/upload-artifact.sh + + - name: Upload MCP Server tarball + if: github.repository == 'stainless-sdks/beeper-desktop-api-typescript' + env: + URL: https://pkg.stainless.com/s?subpackage=mcp-server + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + BUILD_PATH: packages/mcp-server/dist + run: ./scripts/utils/upload-artifact.sh test: timeout-minutes: 10 name: test @@ -79,10 +88,13 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Bootstrap run: ./scripts/bootstrap + - name: Build + run: ./scripts/build + - name: Run tests run: ./scripts/test diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..e95dad5 --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,41 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to NPM in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/beeper/desktop-api-typescript/actions/workflows/publish-npm.yml +name: Publish NPM +on: + workflow_dispatch: + inputs: + path: + description: The path to run the release in, e.g. '.' or 'packages/mcp-server' + required: true + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: | + yarn install + + - name: Publish to NPM + run: | + if [ -n "${{ github.event.inputs.path }}" ]; then + PATHS_RELEASED='[\"${{ github.event.inputs.path }}\"]' + else + PATHS_RELEASED='[\".\", \"packages/mcp-server\"]' + fi + yarn tsn scripts/publish-packages.ts "{ \"paths_released\": \"$PATHS_RELEASED\" }" + env: + NPM_TOKEN: ${{ secrets.BEEPER-DESKTOP_NPM_TOKEN || secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..6fd059f --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,21 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'beeper/desktop-api-typescript' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v4 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + NPM_TOKEN: ${{ secrets.BEEPER-DESKTOP_NPM_TOKEN || secrets.NPM_TOKEN }} diff --git a/.prettierignore b/.prettierignore index 3548c5a..7cc13dd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,4 +4,4 @@ CHANGELOG.md /deno # don't format tsc output, will break source maps -/dist +dist diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/.stats.yml b/.stats.yml index 0096234..46c209f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 13 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-10508b7bd52381c216ed2584b7b0894f70fcc97fbd7f86792e78f72d108157c5.yml openapi_spec_hash: d5f07fd2a363877f4c72da1187b8aaf1 -config_hash: 552d4814bcb4837f1d1acfc0f48773dd +config_hash: ee676f5f5342918d6bf4fde9e77d1c5c diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0c56c82 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## 0.1.0 (2025-08-24) + +Full Changelog: [v0.0.1...v0.1.0](https://github.com/beeper/desktop-api-typescript/compare/v0.0.1...v0.1.0) + +### Features + +* **api:** update via SDK Studio ([cc9a88d](https://github.com/beeper/desktop-api-typescript/commit/cc9a88d50361bb8f0547b24cbdf6d0d6e6b61e87)) +* **api:** update via SDK Studio ([461ee20](https://github.com/beeper/desktop-api-typescript/commit/461ee20c6a395014969262f0b2733ead8b20cfd8)) +* **api:** update via SDK Studio ([f451a9d](https://github.com/beeper/desktop-api-typescript/commit/f451a9d07115959fa0bcb02996ca6809606eeb74)) +* **api:** update via SDK Studio ([8bd03c9](https://github.com/beeper/desktop-api-typescript/commit/8bd03c9935ebd6bc1d974c9bac0372f7e16db3eb)) +* **api:** update via SDK Studio ([c18aaf2](https://github.com/beeper/desktop-api-typescript/commit/c18aaf23a04b97ab55f43d479ef351724b071462)) +* **api:** update via SDK Studio ([34d3a1c](https://github.com/beeper/desktop-api-typescript/commit/34d3a1c3e185bc7fac23983855cb43055244a288)) +* **api:** update via SDK Studio ([5dfa3e9](https://github.com/beeper/desktop-api-typescript/commit/5dfa3e98f5f77f3039907c8f799017990bd5836d)) + + +### Chores + +* update SDK settings ([5cf5745](https://github.com/beeper/desktop-api-typescript/commit/5cf5745b8b07bdbe89099f3d3d25238cf155c33b)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8dbbdce..a4f7c97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,25 +42,25 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ npm install git+ssh://git@github.com:stainless-sdks/beeper-desktop-api-typescript.git +$ npm install git+ssh://git@github.com:beeper/desktop-api-typescript.git ``` Alternatively, to link a local copy of the repo: ```sh # Clone -$ git clone https://www.github.com/stainless-sdks/beeper-desktop-api-typescript -$ cd beeper-desktop-api-typescript +$ git clone https://www.github.com/beeper/desktop-api-typescript +$ cd desktop-api-typescript # With yarn $ yarn link $ cd ../my-package -$ yarn link beeper-desktop-api +$ yarn link desktop-api-typescript # With pnpm $ pnpm link --global $ cd ../my-package -$ pnpm link -—global beeper-desktop-api +$ pnpm link -—global desktop-api-typescript ``` ## Running tests @@ -91,3 +91,17 @@ To format and fix all lint issues automatically: ```sh $ yarn fix ``` + +## Publishing and releases + +Changes made to this repository via the automated release PR pipeline should publish to npm automatically. If +the changes aren't made through the automated pipeline, you may want to make releases manually. + +### Publish with a GitHub workflow + +You can release to package managers by using [the `Publish NPM` GitHub action](https://www.github.com/beeper/desktop-api-typescript/actions/workflows/publish-npm.yml). This requires a setup organization or repository secret to be set up. + +### Publish manually + +If you need to manually release a package, you can run the `bin/publish-npm` script with an `NPM_TOKEN` set on +the environment. diff --git a/LICENSE b/LICENSE index a020613..59424c8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2025 beeper-desktop-api +Copyright 2025 beeperdesktop Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 5437b5e..7399d76 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,35 @@ -# Beeper Desktop API TypeScript API Library +# Beeper Desktop TypeScript API Library -[![NPM version]()](https://npmjs.org/package/beeper-desktop-api) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/beeper-desktop-api) +[![NPM version]()](https://npmjs.org/package/desktop-api-typescript) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/desktop-api-typescript) -This library provides convenient access to the Beeper Desktop API REST API from server-side TypeScript or JavaScript. +This library provides convenient access to the Beeper Desktop REST API from server-side TypeScript or JavaScript. -The REST API documentation can be found on [www.beeper.com](https://www.beeper.com). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [www.beeper.com](https://www.beeper.com/desktop-api). The full API of this library can be found in [api.md](api.md). It is generated with [Stainless](https://www.stainless.com/). ## Installation ```sh -npm install git+ssh://git@github.com:stainless-sdks/beeper-desktop-api-typescript.git +npm install desktop-api-typescript ``` -> [!NOTE] -> Once this package is [published to npm](https://www.stainless.com/docs/guides/publish), this will become: `npm install beeper-desktop-api` - ## Usage The full API of this library can be found in [api.md](api.md). ```js -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'desktop-api-typescript'; -const client = new BeeperDesktopAPI({ - apiKey: process.env['BEEPER_DESKTOP_API_API_KEY'], // This is the default and can be omitted +const client = new BeeperDesktop({ + accessToken: process.env['BEEPER_ACCESS_TOKEN'], // This is the default and can be omitted }); -const getAccounts = await client.getAccounts.list(); +const page = await client.chats.find({ limit: 10, type: 'single' }); +const chat = page.data[0]; -console.log(getAccounts.accounts); +console.log(chat.id); ``` ### Request & Response types @@ -40,13 +38,13 @@ This library includes TypeScript definitions for all request params and response ```ts -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'desktop-api-typescript'; -const client = new BeeperDesktopAPI({ - apiKey: process.env['BEEPER_DESKTOP_API_API_KEY'], // This is the default and can be omitted +const client = new BeeperDesktop({ + accessToken: process.env['BEEPER_ACCESS_TOKEN'], // This is the default and can be omitted }); -const getAccounts: BeeperDesktopAPI.GetAccountListResponse = await client.getAccounts.list(); +const accountsResponse: BeeperDesktop.AccountsResponse = await client.accounts.list(); ``` Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors. @@ -59,15 +57,17 @@ a subclass of `APIError` will be thrown: ```ts -const getAccounts = await client.getAccounts.list().catch(async (err) => { - if (err instanceof BeeperDesktopAPI.APIError) { - console.log(err.status); // 400 - console.log(err.name); // BadRequestError - console.log(err.headers); // {server: 'nginx', ...} - } else { - throw err; - } -}); +const sendResponse = await client.messages + .send({ chatID: '!invalid-chat-id', text: 'Test message' }) + .catch(async (err) => { + if (err instanceof BeeperDesktop.APIError) { + console.log(err.status); // 400 + console.log(err.name); // BadRequestError + console.log(err.headers); // {server: 'nginx', ...} + } else { + throw err; + } + }); ``` Error codes are as follows: @@ -85,7 +85,7 @@ Error codes are as follows: ### Retries -Certain errors will be automatically retried 2 times by default, with a short exponential backoff. +Certain errors will be automatically retried 3 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default. @@ -94,29 +94,29 @@ You can use the `maxRetries` option to configure or disable this: ```js // Configure the default for all requests: -const client = new BeeperDesktopAPI({ +const client = new BeeperDesktop({ maxRetries: 0, // default is 2 }); // Or, configure per-request: -await client.getAccounts.list({ +await client.accounts.list({ maxRetries: 5, }); ``` ### Timeouts -Requests time out after 1 minute by default. You can configure this with a `timeout` option: +Requests time out after 30 seconds by default. You can configure this with a `timeout` option: ```ts // Configure the default for all requests: -const client = new BeeperDesktopAPI({ - timeout: 20 * 1000, // 20 seconds (default is 1 minute) +const client = new BeeperDesktop({ + timeout: 20 * 1000, // 20 seconds (default is 30 seconds) }); // Override per-request: -await client.getAccounts.list({ +await client.accounts.list({ timeout: 5 * 1000, }); ``` @@ -125,6 +125,37 @@ On timeout, an `APIConnectionTimeoutError` is thrown. Note that requests which time out will be [retried twice by default](#retries). +## Auto-pagination + +List methods in the BeeperDesktop API are paginated. +You can use the `for await … of` syntax to iterate through items across all pages: + +```ts +async function fetchAllMessages(params) { + const allMessages = []; + // Automatically fetches more pages as needed. + for await (const message of client.messages.search({ limit: 20, query: 'meeting' })) { + allMessages.push(message); + } + return allMessages; +} +``` + +Alternatively, you can request a single page at a time: + +```ts +let page = await client.messages.search({ limit: 20, query: 'meeting' }); +for (const message of page.data) { + console.log(message); +} + +// Convenience methods are provided for manually paginating: +while (page.hasNextPage()) { + page = await page.getNextPage(); + // ... +} +``` + ## Advanced Usage ### Accessing raw Response data (e.g., headers) @@ -137,15 +168,15 @@ Unlike `.asResponse()` this method consumes the body, returning once it is parse ```ts -const client = new BeeperDesktopAPI(); +const client = new BeeperDesktop(); -const response = await client.getAccounts.list().asResponse(); +const response = await client.accounts.list().asResponse(); console.log(response.headers.get('X-My-Header')); console.log(response.statusText); // access the underlying Response object -const { data: getAccounts, response: raw } = await client.getAccounts.list().withResponse(); +const { data: accountsResponse, response: raw } = await client.accounts.list().withResponse(); console.log(raw.headers.get('X-My-Header')); -console.log(getAccounts.accounts); +console.log(accountsResponse.accounts); ``` ### Logging @@ -158,13 +189,13 @@ console.log(getAccounts.accounts); The log level can be configured in two ways: -1. Via the `BEEPER_DESKTOP_API_LOG` environment variable +1. Via the `BEEPER-DESKTOP_LOG` environment variable 2. Using the `logLevel` client option (overrides the environment variable if set) ```ts -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'desktop-api-typescript'; -const client = new BeeperDesktopAPI({ +const client = new BeeperDesktop({ logLevel: 'debug', // Show all log messages }); ``` @@ -190,13 +221,13 @@ When providing a custom logger, the `logLevel` option still controls which messa below the configured level will not be sent to your logger. ```ts -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'desktop-api-typescript'; import pino from 'pino'; const logger = pino(); -const client = new BeeperDesktopAPI({ - logger: logger.child({ name: 'BeeperDesktopAPI' }), +const client = new BeeperDesktop({ + logger: logger.child({ name: 'BeeperDesktop' }), logLevel: 'debug', // Send all messages to pino, allowing it to filter }); ``` @@ -225,7 +256,7 @@ parameter. This library doesn't validate at runtime that the request matches the send will be sent as-is. ```ts -client.getAccounts.list({ +client.chats.find({ // ... // @ts-expect-error baz is not yet public baz: 'undocumented option', @@ -259,10 +290,10 @@ globalThis.fetch = fetch; Or pass it to the client: ```ts -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'desktop-api-typescript'; import fetch from 'my-fetch'; -const client = new BeeperDesktopAPI({ fetch }); +const client = new BeeperDesktop({ fetch }); ``` ### Fetch options @@ -270,9 +301,9 @@ const client = new BeeperDesktopAPI({ fetch }); If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.) ```ts -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'desktop-api-typescript'; -const client = new BeeperDesktopAPI({ +const client = new BeeperDesktop({ fetchOptions: { // `RequestInit` options }, @@ -287,11 +318,11 @@ options to requests: **Node** [[docs](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md#example---proxyagent-with-fetch)] ```ts -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'desktop-api-typescript'; import * as undici from 'undici'; const proxyAgent = new undici.ProxyAgent('http://localhost:8888'); -const client = new BeeperDesktopAPI({ +const client = new BeeperDesktop({ fetchOptions: { dispatcher: proxyAgent, }, @@ -301,9 +332,9 @@ const client = new BeeperDesktopAPI({ **Bun** [[docs](https://bun.sh/guides/http/proxy)] ```ts -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'desktop-api-typescript'; -const client = new BeeperDesktopAPI({ +const client = new BeeperDesktop({ fetchOptions: { proxy: 'http://localhost:8888', }, @@ -313,10 +344,10 @@ const client = new BeeperDesktopAPI({ **Deno** [[docs](https://docs.deno.com/api/deno/~/Deno.createHttpClient)] ```ts -import BeeperDesktopAPI from 'npm:beeper-desktop-api'; +import BeeperDesktop from 'npm:desktop-api-typescript'; const httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } }); -const client = new BeeperDesktopAPI({ +const client = new BeeperDesktop({ fetchOptions: { client: httpClient, }, @@ -335,7 +366,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/beeper-desktop-api-typescript/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/beeper/desktop-api-typescript/issues) with questions, bugs, or suggestions. ## Requirements @@ -343,7 +374,6 @@ TypeScript >= 4.9 is supported. The following runtimes are supported: -- Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more) - Node.js 20 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions. - Deno v1.28.0 or higher. - Bun 1.0 or later. @@ -351,6 +381,24 @@ The following runtimes are supported: - Vercel Edge Runtime. - Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time). - Nitro v2.6 or greater. +- Web browsers: disabled by default to avoid exposing your secret API credentials. Enable browser support by explicitly setting `dangerouslyAllowBrowser` to true'. +
+ More explanation + + ### Why is this dangerous? + + Enabling the `dangerouslyAllowBrowser` option can be dangerous because it exposes your secret API credentials in the client-side code. Web browsers are inherently less secure than server environments, + any user with access to the browser can potentially inspect, extract, and misuse these credentials. This could lead to unauthorized access using your credentials and potentially compromise sensitive data or functionality. + + ### When might this not be dangerous? + + In certain scenarios where enabling browser support might not pose significant risks: + + - Internal Tools: If the application is used solely within a controlled internal environment where the users are trusted, the risk of credential exposure can be mitigated. + - Public APIs with Limited Scope: If your API has very limited scope and the exposed credentials do not grant access to sensitive data or critical operations, the potential impact of exposure is reduced. + - Development or debugging purpose: Enabling this feature temporarily might be acceptable, provided the credentials are short-lived, aren't also used in production environments, or are frequently rotated. + +
Note that React Native is not supported at this time. diff --git a/SECURITY.md b/SECURITY.md index 5965b61..d6a0717 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,9 +16,9 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Beeper Desktop API, please follow the respective company's security reporting guidelines. +or products provided by Beeper Desktop, please follow the respective company's security reporting guidelines. -### Beeper Desktop API Terms and Policies +### Beeper Desktop Terms and Policies Please contact help@beeper.com for any questions or concerns regarding the security of our services. diff --git a/api.md b/api.md index 360db11..5c0de07 100644 --- a/api.md +++ b/api.md @@ -1,105 +1,91 @@ -# GetAccounts +# Shared Types: -- GetAccountListResponse +- Attachment +- BaseResponse +- Error +- Reaction +- User -Methods: - -- client.getAccounts.list() -> GetAccountListResponse - -# FocusApp - -Types: - -- BaseResponse - -Methods: - -- client.focusApp.open({ ...params }) -> BaseResponse - -# DraftMessage - -Methods: - -- client.draftMessage.create({ ...params }) -> BaseResponse - -# GetLinkToChat +# Accounts Types: -- GetLinkToChatCreateResponse +- Account +- AccountsResponse Methods: -- client.getLinkToChat.create({ ...params }) -> GetLinkToChatCreateResponse +- client.accounts.list() -> AccountsResponse -# GetChat +# App Types: -- User -- GetChatRetrieveResponse +- FocusRequest Methods: -- client.getChat.retrieve({ ...params }) -> GetChatRetrieveResponse | null +- client.app.focus({ ...params }) -> BaseResponse -# FindChats +# Messages Types: -- FindChatListResponse +- DraftRequest +- Message +- SearchRequest +- SearchResponse +- SendRequest +- SendResponse Methods: -- client.findChats.list({ ...params }) -> FindChatListResponse +- client.messages.draft({ ...params }) -> BaseResponse +- client.messages.search({ ...params }) -> MessagesCursorID +- client.messages.send({ ...params }) -> SendResponse -# SearchMessages +# Chats Types: -- SearchMessageSearchResponse +- ArchiveRequest +- Chat +- FindChatsRequest +- FindChatsResponse +- GetChatRequest +- GetChatResponse +- LinkRequest +- LinkResponse Methods: -- client.searchMessages.search({ ...params }) -> SearchMessageSearchResponse +- client.chats.retrieve({ ...params }) -> GetChatResponse | null +- client.chats.archive({ ...params }) -> BaseResponse +- client.chats.find({ ...params }) -> ChatsCursorID +- client.chats.getLink({ ...params }) -> LinkResponse -# SendMessage +# Reminders Types: -- SendMessageSendResponse - -Methods: - -- client.sendMessage.send({ ...params }) -> SendMessageSendResponse - -# ArchiveChat - -Methods: - -- client.archiveChat.archive({ ...params }) -> BaseResponse - -# SetChatReminder - -Methods: - -- client.setChatReminder.create({ ...params }) -> BaseResponse - -# ClearChatReminder +- ClearReminderRequest +- SetReminderRequest Methods: -- client.clearChatReminder.clear({ ...params }) -> BaseResponse +- client.reminders.clear({ ...params }) -> BaseResponse +- client.reminders.set({ ...params }) -> BaseResponse # OAuth Types: -- OAuthRetrieveUserInfoResponse +- RevokeRequest +- UserInfo Methods: -- client.oauth.retrieveUserInfo() -> OAuthRetrieveUserInfoResponse +- client.oauth.getUserInfo() -> UserInfo - client.oauth.revokeToken({ ...params }) -> void diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..e4b6d58 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${NPM_TOKEN}" ]; then + errors+=("The NPM_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" + diff --git a/eslint.config.mjs b/eslint.config.mjs index 213af9e..43c7801 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,7 +25,7 @@ export default tseslint.config( { patterns: [ { - regex: '^beeper-desktop-api(/.*)?', + regex: '^desktop-api-typescript(/.*)?', message: 'Use a relative import, not a package import.', }, ], @@ -34,7 +34,7 @@ export default tseslint.config( }, }, { - files: ['tests/**', 'examples/**'], + files: ['tests/**', 'examples/**', 'packages/**'], rules: { 'no-restricted-imports': 'off', }, diff --git a/jest.config.ts b/jest.config.ts index a54effa..b260153 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -7,8 +7,8 @@ const config: JestConfigWithTsJest = { '^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }], }, moduleNameMapper: { - '^beeper-desktop-api$': '/src/index.ts', - '^beeper-desktop-api/(.*)$': '/src/$1', + '^desktop-api-typescript$': '/src/index.ts', + '^desktop-api-typescript/(.*)$': '/src/$1', }, modulePathIgnorePatterns: [ '/ecosystem-tests/', diff --git a/package.json b/package.json index 16a8aad..b3ae5cb 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "beeper-desktop-api", - "version": "0.0.1", - "description": "The official TypeScript library for the Beeper Desktop API API", - "author": "Beeper Desktop API ", + "name": "desktop-api-typescript", + "version": "0.1.0", + "description": "The official TypeScript library for the Beeper Desktop API", + "author": "Beeper Desktop ", "types": "dist/index.d.ts", "main": "dist/index.js", "type": "commonjs", - "repository": "github:stainless-sdks/beeper-desktop-api-typescript", + "repository": "github:beeper/desktop-api-typescript", "license": "MIT", "packageManager": "yarn@1.22.22", "files": [ diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md new file mode 100644 index 0000000..2670b0e --- /dev/null +++ b/packages/mcp-server/README.md @@ -0,0 +1,265 @@ +# Beeper Desktop TypeScript MCP Server + +It is generated with [Stainless](https://www.stainless.com/). + +## Installation + +### Direct invocation + +You can run the MCP Server directly via `npx`: + +```sh +export BEEPER_ACCESS_TOKEN="My Access Token" +npx -y beeper/desktop-api-typescript-mcp@latest +``` + +### Via MCP Client + +There is a partial list of existing clients at [modelcontextprotocol.io](https://modelcontextprotocol.io/clients). If you already +have a client, consult their documentation to install the MCP server. + +For clients with a configuration JSON, it might look something like this: + +```json +{ + "mcpServers": { + "desktop_api_typescript_api": { + "command": "npx", + "args": ["-y", "beeper/desktop-api-typescript-mcp", "--client=claude", "--tools=all"], + "env": { + "BEEPER_ACCESS_TOKEN": "My Access Token" + } + } + } +} +``` + +## Exposing endpoints to your MCP Client + +There are two ways to expose endpoints as tools in the MCP server: + +1. Exposing one tool per endpoint, and filtering as necessary +2. Exposing a set of tools to dynamically discover and invoke endpoints from the API + +### Filtering endpoints and tools + +You can run the package on the command line to discover and filter the set of tools that are exposed by the +MCP Server. This can be helpful for large APIs where including all endpoints at once is too much for your AI's +context window. + +You can filter by multiple aspects: + +- `--tool` includes a specific tool by name +- `--resource` includes all tools under a specific resource, and can have wildcards, e.g. `my.resource*` +- `--operation` includes just read (get/list) or just write operations + +### Dynamic tools + +If you specify `--tools=dynamic` to the MCP server, instead of exposing one tool per endpoint in the API, it will +expose the following tools: + +1. `list_api_endpoints` - Discovers available endpoints, with optional filtering by search query +2. `get_api_endpoint_schema` - Gets detailed schema information for a specific endpoint +3. `invoke_api_endpoint` - Executes any endpoint with the appropriate parameters + +This allows you to have the full set of API endpoints available to your MCP Client, while not requiring that all +of their schemas be loaded into context at once. Instead, the LLM will automatically use these tools together to +search for, look up, and invoke endpoints dynamically. However, due to the indirect nature of the schemas, it +can struggle to provide the correct properties a bit more than when tools are imported explicitly. Therefore, +you can opt-in to explicit tools, the dynamic tools, or both. + +See more information with `--help`. + +All of these command-line options can be repeated, combined together, and have corresponding exclusion versions (e.g. `--no-tool`). + +Use `--list` to see the list of available tools, or see below. + +### Specifying the MCP Client + +Different clients have varying abilities to handle arbitrary tools and schemas. + +You can specify the client you are using with the `--client` argument, and the MCP server will automatically +serve tools and schemas that are more compatible with that client. + +- `--client=`: Set all capabilities based on a known MCP client + + - Valid values: `openai-agents`, `claude`, `claude-code`, `cursor` + - Example: `--client=cursor` + +Additionally, if you have a client not on the above list, or the client has gotten better +over time, you can manually enable or disable certain capabilities: + +- `--capability=`: Specify individual client capabilities + - Available capabilities: + - `top-level-unions`: Enable support for top-level unions in tool schemas + - `valid-json`: Enable JSON string parsing for arguments + - `refs`: Enable support for $ref pointers in schemas + - `unions`: Enable support for union types (anyOf) in schemas + - `formats`: Enable support for format validations in schemas (e.g. date-time, email) + - `tool-name-length=N`: Set maximum tool name length to N characters + - Example: `--capability=top-level-unions --capability=tool-name-length=40` + - Example: `--capability=top-level-unions,tool-name-length=40` + +### Examples + +1. Filter for read operations on cards: + +```bash +--resource=cards --operation=read +``` + +2. Exclude specific tools while including others: + +```bash +--resource=cards --no-tool=create_cards +``` + +3. Configure for Cursor client with custom max tool name length: + +```bash +--client=cursor --capability=tool-name-length=40 +``` + +4. Complex filtering with multiple criteria: + +```bash +--resource=cards,accounts --operation=read --tag=kyc --no-tool=create_cards +``` + +## Running remotely + +Launching the client with `--transport=http` launches the server as a remote server using Streamable HTTP transport. The `--port` setting can choose the port it will run on, and the `--socket` setting allows it to run on a Unix socket. + +Authorization can be provided via the `Authorization` header using the Bearer scheme. + +Additionally, authorization can be provided via the following headers: +| Header | Equivalent client option | Security scheme | +| ----------------------- | ------------------------ | --------------- | +| `x-beeper-access-token` | `accessToken` | bearerAuth | + +A configuration JSON for this server might look like this, assuming the server is hosted at `http://localhost:3000`: + +```json +{ + "mcpServers": { + "desktop_api_typescript_api": { + "url": "http://localhost:3000", + "headers": { + "Authorization": "Bearer " + } + } + } +} +``` + +The command-line arguments for filtering tools and specifying clients can also be used as query parameters in the URL. +For example, to exclude specific tools while including others, use the URL: + +``` +http://localhost:3000?resource=cards&resource=accounts&no_tool=create_cards +``` + +Or, to configure for the Cursor client, with a custom max tool name length, use the URL: + +``` +http://localhost:3000?client=cursor&capability=tool-name-length%3D40 +``` + +## Importing the tools and server individually + +```js +// Import the server, generated endpoints, or the init function +import { server, endpoints, init } from "beeper/desktop-api-typescript-mcp/server"; + +// import a specific tool +import listAccounts from "beeper/desktop-api-typescript-mcp/tools/accounts/list-accounts"; + +// initialize the server and all endpoints +init({ server, endpoints }); + +// manually start server +const transport = new StdioServerTransport(); +await server.connect(transport); + +// or initialize your own server with specific tools +const myServer = new McpServer(...); + +// define your own endpoint +const myCustomEndpoint = { + tool: { + name: 'my_custom_tool', + description: 'My custom tool', + inputSchema: zodToJsonSchema(z.object({ a_property: z.string() })), + }, + handler: async (client: client, args: any) => { + return { myResponse: 'Hello world!' }; + }) +}; + +// initialize the server with your custom endpoints +init({ server: myServer, endpoints: [listAccounts, myCustomEndpoint] }); +``` + +## Available Tools + +The following tools are available in this MCP server. + +### Resource `accounts`: + +- `list_accounts` (`read`): List connected Beeper accounts available on this device. + - When to use: select account context before account-scoped operations. + - Scope: only accounts currently Connected on this device are included. + Returns: connected accounts. + +### Resource `app`: + +- `focus_app` (`write`): Bring Beeper Desktop to the foreground on this device. Optionally focuses a specific chat if chatID is provided. + - When to use: open Beeper, or jump to a specific chat. + - Constraints: requires Beeper Desktop running locally; no-op in headless environments. + - Idempotent: safe to call repeatedly. Returns an error if chatID is not found. + Returns: success. + +### Resource `messages`: + +- `draft_messages` (`write`): Draft a message in a specific chat. This will be placed in the message input field without sending +- `search_messages` (`read`): Search messages across chats using Beeper's message index. + - When to use: find messages by text and/or filters (chatIDs, accountIDs, chatType, media type filters, sender, date ranges). + - CRITICAL: Query is LITERAL WORD MATCHING, NOT semantic search! Only finds messages containing these EXACT words. + • ✅ RIGHT: query="dinner" or query="sick" or query="error" (single words users type) + • ❌ WRONG: query="dinner plans tonight" or query="health issues" (phrases/concepts) + • The query matches ALL words provided (in any order). Example: query="flight booking" finds messages with both "flight" AND "booking". + - Media filters: Use onlyWithMedia for any media, or specific filters like onlyWithVideo, onlyWithImage, onlyWithLink, onlyWithFile for specific types. + - Pagination: use 'oldestCursor' + direction='before' for older; 'newestCursor' + direction='after' for newer. + - Performance: provide chatIDs/accountIDs when known. Omitted 'query' returns results based on filters only. Partial matches enabled; 'excludeLowPriority' defaults to true. + - Workflow tip: To search messages in specific conversations: 1) Use find-chats to get chatIDs, 2) Use search-messages with those chatIDs. + - IMPORTANT: Chat names vary widely. ASK the user for clarification: + • "Which chat do you mean by family?" (could be "The Smiths", "Mom Dad Kids", etc.) + • "What's the name of your work chat?" (could be "Team", company name, project name) + • "Who are the participants?" (use participantQuery in find-chats) + Returns: matching messages and referenced chats. +- `send_messages` (`write`): Send a text message to a specific chat. Supports replying to existing messages. Returns the sent message ID and a deeplink to the chat + +### Resource `chats`: + +- `retrieve_chats` (`read`): Retrieve chat details: metadata, participants (limited), and latest message. + - When to use: fetch a complete view of a chat beyond what search returns. + - Constraints: not available for iMessage chats ('imsg##'). Participants limited by 'maxParticipantCount' (default 20, max 500). + Returns: chat details.Agents: ALWAYS use linkToChat to make clickable links in your response +- `archive_chats` (`write`): Archive or unarchive a chat. Set archived=true to move to archive, archived=false to move back to inbox +- `find_chats` (`read`): Search and filter conversations across all messaging accounts. + - When to use: browse chats by inbox (primary/low-priority/archive), type, unread status, or search terms. + - Pagination: use cursor + direction for pagination. + - Performance: provide accountIDs when known for faster filtering. + Returns: matching chats with pagination. + Agents: ALWAYS use linkToChat to make clickable links in your response +- `get_link_chats` (`write`): Generate a deep link to a specific chat or message. This link can be used to open the chat directly in the Beeper app. + +### Resource `reminders`: + +- `clear_reminders` (`write`): Clear an existing reminder from a chat +- `set_reminders` (`write`): Set a reminder for a chat at a specific time + +### Resource `oauth`: + +- `get_user_info_oauth` (`read`): Returns information about the authenticated user/token +- `revoke_token_oauth` (`write`): Revoke an access token or refresh token (RFC 7009) diff --git a/packages/mcp-server/build b/packages/mcp-server/build new file mode 100644 index 0000000..3a39730 --- /dev/null +++ b/packages/mcp-server/build @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -exuo pipefail + +rm -rf dist; mkdir dist + +# Copy src to dist/src and build from dist/src into dist, so that +# the source map for index.js.map will refer to ./src/index.ts etc +cp -rp src README.md dist + +for file in LICENSE; do + if [ -e "../../${file}" ]; then cp "../../${file}" dist; fi +done + +for file in CHANGELOG.md; do + if [ -e "${file}" ]; then cp "${file}" dist; fi +done + +# this converts the export map paths for the dist directory +# and does a few other minor things +PKG_JSON_PATH=../../packages/mcp-server/package.json node ../../scripts/utils/make-dist-package-json.cjs > dist/package.json + +# updates the `desktop-api-typescript` dependency to point to NPM +node scripts/postprocess-dist-package-json.cjs + +# build to .js/.mjs/.d.ts files +./node_modules/.bin/tsc-multi + +cp tsconfig.dist-src.json dist/src/tsconfig.json + +chmod +x dist/index.js + +DIST_PATH=./dist PKG_IMPORT_PATH=beeper/desktop-api-typescript-mcp/ node ../../scripts/utils/postprocess-files.cjs diff --git a/packages/mcp-server/jest.config.ts b/packages/mcp-server/jest.config.ts new file mode 100644 index 0000000..0504f5e --- /dev/null +++ b/packages/mcp-server/jest.config.ts @@ -0,0 +1,17 @@ +import type { JestConfigWithTsJest } from 'ts-jest'; + +const config: JestConfigWithTsJest = { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + transform: { + '^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }], + }, + moduleNameMapper: { + '^beeper/desktop-api-typescript-mcp$': '/src/index.ts', + '^beeper/desktop-api-typescript-mcp/(.*)$': '/src/$1', + }, + modulePathIgnorePatterns: ['/dist/'], + testPathIgnorePatterns: ['scripts'], +}; + +export default config; diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json new file mode 100644 index 0000000..19d5994 --- /dev/null +++ b/packages/mcp-server/package.json @@ -0,0 +1,85 @@ +{ + "name": "beeper/desktop-api-typescript-mcp", + "version": "0.1.0", + "description": "The official MCP Server for the Beeper Desktop API", + "author": "Beeper Desktop ", + "types": "dist/index.d.ts", + "main": "dist/index.js", + "type": "commonjs", + "repository": { + "type": "git", + "url": "git+https://github.com/beeper/desktop-api-typescript.git", + "directory": "packages/mcp-server" + }, + "homepage": "https://github.com/beeper/desktop-api-typescript/tree/main/packages/mcp-server#readme", + "license": "MIT", + "packageManager": "yarn@1.22.22", + "private": false, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "jest", + "build": "bash ./build", + "prepack": "echo 'to pack, run yarn build && (cd dist; yarn pack)' && exit 1", + "prepublishOnly": "echo 'to publish, run yarn build && (cd dist; yarn publish)' && exit 1", + "format": "prettier --write --cache --cache-strategy metadata . !dist", + "prepare": "npm run build", + "tsn": "ts-node -r tsconfig-paths/register", + "lint": "eslint --ext ts,js .", + "fix": "eslint --fix --ext ts,js ." + }, + "dependencies": { + "desktop-api-typescript": "file:../../dist/", + "@cloudflare/cabidela": "^0.2.4", + "@modelcontextprotocol/sdk": "^1.11.5", + "@valtown/deno-http-worker": "^0.0.21", + "cors": "^2.8.5", + "express": "^5.1.0", + "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz", + "qs": "^6.14.0", + "yargs": "^17.7.2", + "zod": "^3.25.20", + "zod-to-json-schema": "^3.24.5", + "zod-validation-error": "^4.0.1" + }, + "bin": { + "mcp-server": "dist/index.js" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jest": "^29.4.0", + "@types/qs": "^6.14.0", + "@types/yargs": "^17.0.8", + "@typescript-eslint/eslint-plugin": "8.31.1", + "@typescript-eslint/parser": "8.31.1", + "eslint": "^8.49.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-unused-imports": "^3.0.0", + "jest": "^29.4.0", + "prettier": "^3.0.0", + "ts-jest": "^29.1.0", + "ts-morph": "^19.0.0", + "ts-node": "^10.5.0", + "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.9/tsc-multi.tgz", + "tsconfig-paths": "^4.0.0", + "typescript": "5.8.3" + }, + "imports": { + "beeper/desktop-api-typescript-mcp": ".", + "beeper/desktop-api-typescript-mcp/*": "./src/*" + }, + "exports": { + ".": { + "require": "./dist/index.js", + "default": "./dist/index.mjs" + }, + "./*.mjs": "./dist/*.mjs", + "./*.js": "./dist/*.js", + "./*": { + "require": "./dist/*.js", + "default": "./dist/*.mjs" + } + } +} diff --git a/packages/mcp-server/scripts/postprocess-dist-package-json.cjs b/packages/mcp-server/scripts/postprocess-dist-package-json.cjs new file mode 100644 index 0000000..7e73621 --- /dev/null +++ b/packages/mcp-server/scripts/postprocess-dist-package-json.cjs @@ -0,0 +1,12 @@ +const fs = require('fs'); +const pkgJson = require('../dist/package.json'); +const parentPkgJson = require('../../../package.json'); + +for (const dep in pkgJson.dependencies) { + // ensure we point to NPM instead of a local directory + if (dep === 'desktop-api-typescript') { + pkgJson.dependencies[dep] = '^' + parentPkgJson.version; + } +} + +fs.writeFileSync('dist/package.json', JSON.stringify(pkgJson, null, 2)); diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts new file mode 100644 index 0000000..15ce7f5 --- /dev/null +++ b/packages/mcp-server/src/code-tool-paths.cts @@ -0,0 +1,3 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export const workerPath = require.resolve('./code-tool-worker.mjs'); diff --git a/packages/mcp-server/src/code-tool-types.ts b/packages/mcp-server/src/code-tool-types.ts new file mode 100644 index 0000000..a8ba145 --- /dev/null +++ b/packages/mcp-server/src/code-tool-types.ts @@ -0,0 +1,14 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { ClientOptions } from 'desktop-api-typescript'; + +export type WorkerInput = { + opts: ClientOptions; + code: string; +}; +export type WorkerSuccess = { + result: unknown | null; + logLines: string[]; + errLines: string[]; +}; +export type WorkerError = { message: string | undefined }; diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts new file mode 100644 index 0000000..7638528 --- /dev/null +++ b/packages/mcp-server/src/code-tool-worker.ts @@ -0,0 +1,46 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import util from 'node:util'; +import { WorkerInput, WorkerSuccess, WorkerError } from './code-tool-types'; +import { BeeperDesktop } from 'desktop-api-typescript'; + +const fetch = async (req: Request): Promise => { + const { opts, code } = (await req.json()) as WorkerInput; + const client = new BeeperDesktop({ + ...opts, + }); + + const logLines: string[] = []; + const errLines: string[] = []; + const console = { + log: (...args: unknown[]) => { + logLines.push(util.format(...args)); + }, + error: (...args: unknown[]) => { + errLines.push(util.format(...args)); + }, + }; + try { + let run_ = async (client: any) => {}; + eval(` + ${code} + run_ = run; + `); + const result = await run_(client); + return Response.json({ + result, + logLines, + errLines, + } satisfies WorkerSuccess); + } catch (e) { + const message = e instanceof Error ? e.message : undefined; + return Response.json( + { + message, + } satisfies WorkerError, + { status: 400, statusText: 'Code execution error' }, + ); + } +}; + +export default { fetch }; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts new file mode 100644 index 0000000..ec45187 --- /dev/null +++ b/packages/mcp-server/src/code-tool.ts @@ -0,0 +1,144 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { dirname } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import BeeperDesktop, { ClientOptions } from 'desktop-api-typescript'; +import { Endpoint, ContentBlock, Metadata } from './tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; + +import { newDenoHTTPWorker } from '@valtown/deno-http-worker'; +import { WorkerInput, WorkerError, WorkerSuccess } from './code-tool-types'; +import { workerPath } from './code-tool-paths.cjs'; + +/** + * A tool that runs code against a copy of the SDK. + * + * Instead of exposing every endpoint as it's own tool, which uses up too many tokens for LLMs to use at once, + * we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then + * a generic endpoint that can be used to invoke any endpoint with the provided arguments. + * + * @param endpoints - The endpoints to include in the list. + */ +export function codeTool(): Endpoint { + const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; + const tool: Tool = { + name: 'execute', + description: + 'Runs Typescript code to interact with the API.\nYou are a skilled programmer writing code to interface with the service.\nDefine an async function named "run" that takes a single parameter of an initialized client, and it will be run.\nDo not initialize a client, but instead use the client that you are given as a parameter.\nYou will be returned anything that your function returns, plus the results of any console.log statements.\nIf any code triggers an error, the tool will return an error response, so you do not need to add error handling unless you want to output something more helpful than the raw error.\nIt is not necessary to add comments to code, unless by adding those comments you believe that you can generate better code.\nThis code will run in a container, and you will not be able to use fetch or otherwise interact with the network calls other than through the client you are given.\nAny variables you define won\'t live between successive uses of this call, so make sure to return or log any data you might need later.', + inputSchema: { type: 'object', properties: { code: { type: 'string' } } }, + }; + + const handler = async (client: BeeperDesktop, args: unknown) => { + const baseURLHostname = new URL(client.baseURL).hostname; + const { code } = args as { code: string }; + + const worker = await newDenoHTTPWorker(pathToFileURL(workerPath), { + runFlags: [ + `--node-modules-dir=manual`, + `--allow-read=code-tool-worker.mjs,${workerPath.replace(/([\/\\]node_modules)[\/\\].+$/, '$1')}/`, + `--allow-net=${baseURLHostname}`, + // Allow environment variables because instantiating the client will try to read from them, + // even though they are not set. + '--allow-env', + ], + printOutput: true, + spawnOptions: { + cwd: dirname(workerPath), + }, + }); + + try { + const resp = await new Promise((resolve, reject) => { + worker.addEventListener('exit', (exitCode) => { + reject(new Error(`Worker exited with code ${exitCode}`)); + }); + + const opts: ClientOptions = { + baseURL: client.baseURL, + accessToken: client.accessToken, + defaultHeaders: { + 'X-Stainless-MCP': 'true', + }, + }; + + const req = worker.request( + 'http://localhost', + { + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + }, + (resp) => { + const body: Uint8Array[] = []; + resp.on('error', (err) => { + reject(err); + }); + resp.on('data', (chunk) => { + body.push(chunk); + }); + resp.on('end', () => { + resolve( + new Response(Buffer.concat(body).toString(), { + status: resp.statusCode ?? 200, + headers: resp.headers as any, + }), + ); + }); + }, + ); + + const body = JSON.stringify({ + opts, + code, + } satisfies WorkerInput); + + req.write(body, (err) => { + if (err !== null && err !== undefined) { + reject(err); + } + }); + + req.end(); + }); + + if (resp.status === 200) { + const { result, logLines, errLines } = (await resp.json()) as WorkerSuccess; + const returnOutput: ContentBlock | null = + result === null ? null + : result === undefined ? null + : { + type: 'text', + text: typeof result === 'string' ? (result as string) : JSON.stringify(result), + }; + const logOutput: ContentBlock | null = + logLines.length === 0 ? + null + : { + type: 'text', + text: logLines.join('\n'), + }; + const errOutput: ContentBlock | null = + errLines.length === 0 ? + null + : { + type: 'text', + text: 'Error output:\n' + errLines.join('\n'), + }; + return { + content: [returnOutput, logOutput, errOutput].filter((block) => block !== null), + }; + } else { + const { message } = (await resp.json()) as WorkerError; + throw new Error(message); + } + } catch (e) { + throw e; + } finally { + worker.terminate(); + } + }; + + return { metadata, tool, handler }; +} diff --git a/packages/mcp-server/src/compat.ts b/packages/mcp-server/src/compat.ts new file mode 100644 index 0000000..f84053c --- /dev/null +++ b/packages/mcp-server/src/compat.ts @@ -0,0 +1,483 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; +import { Endpoint } from './tools'; + +export interface ClientCapabilities { + topLevelUnions: boolean; + validJson: boolean; + refs: boolean; + unions: boolean; + formats: boolean; + toolNameLength: number | undefined; +} + +export const defaultClientCapabilities: ClientCapabilities = { + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, +}; + +export const ClientType = z.enum(['openai-agents', 'claude', 'claude-code', 'cursor', 'infer']); +export type ClientType = z.infer; + +// Client presets for compatibility +// Note that these could change over time as models get better, so this is +// a best effort. +export const knownClients: Record, ClientCapabilities> = { + 'openai-agents': { + topLevelUnions: false, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }, + claude: { + topLevelUnions: true, + validJson: false, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }, + 'claude-code': { + topLevelUnions: false, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }, + cursor: { + topLevelUnions: false, + validJson: true, + refs: false, + unions: false, + formats: false, + toolNameLength: 50, + }, +}; + +/** + * Attempts to parse strings into JSON objects + */ +export function parseEmbeddedJSON(args: Record, schema: Record) { + let updated = false; + const newArgs: Record = Object.assign({}, args); + + for (const [key, value] of Object.entries(newArgs)) { + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + // Only parse if result is a plain object (not array, null, or primitive) + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + newArgs[key] = parsed; + updated = true; + } + } catch (e) { + // Not valid JSON, leave as is + } + } + } + + if (updated) { + return newArgs; + } + + return args; +} + +export type JSONSchema = { + type?: string; + properties?: Record; + required?: string[]; + anyOf?: JSONSchema[]; + $ref?: string; + $defs?: Record; + [key: string]: any; +}; + +/** + * Truncates tool names to the specified length while ensuring uniqueness. + * If truncation would cause duplicate names, appends a number to make them unique. + */ +export function truncateToolNames(names: string[], maxLength: number): Map { + if (maxLength <= 0) { + return new Map(); + } + + const renameMap = new Map(); + const usedNames = new Set(); + + const toTruncate = names.filter((name) => name.length > maxLength); + + if (toTruncate.length === 0) { + return renameMap; + } + + const willCollide = + new Set(toTruncate.map((name) => name.slice(0, maxLength - 1))).size < toTruncate.length; + + if (!willCollide) { + for (const name of toTruncate) { + const truncatedName = name.slice(0, maxLength); + renameMap.set(name, truncatedName); + } + } else { + const baseLength = maxLength - 1; + + for (const name of toTruncate) { + const baseName = name.slice(0, baseLength); + let counter = 1; + + while (usedNames.has(baseName + counter)) { + counter++; + } + + const finalName = baseName + counter; + renameMap.set(name, finalName); + usedNames.add(finalName); + } + } + + return renameMap; +} + +/** + * Removes top-level unions from a tool by splitting it into multiple tools, + * one for each variant in the union. + */ +export function removeTopLevelUnions(tool: Tool): Tool[] { + const inputSchema = tool.inputSchema as JSONSchema; + const variants = inputSchema.anyOf; + + if (!variants || !Array.isArray(variants) || variants.length === 0) { + return [tool]; + } + + const defs = inputSchema.$defs || {}; + + return variants.map((variant, index) => { + const variantSchema: JSONSchema = { + ...inputSchema, + ...variant, + type: 'object', + properties: { + ...(inputSchema.properties || {}), + ...(variant.properties || {}), + }, + }; + + delete variantSchema.anyOf; + + if (!variantSchema['description']) { + variantSchema['description'] = tool.description; + } + + const usedDefs = findUsedDefs(variant, defs); + if (Object.keys(usedDefs).length > 0) { + variantSchema.$defs = usedDefs; + } else { + delete variantSchema.$defs; + } + + return { + ...tool, + name: `${tool.name}_${toSnakeCase(variant['title'] || `variant${index + 1}`)}`, + description: variant['description'] || tool.description, + inputSchema: variantSchema, + } as Tool; + }); +} + +function findUsedDefs( + schema: JSONSchema, + defs: Record, + visited: Set = new Set(), +): Record { + const usedDefs: Record = {}; + + if (typeof schema !== 'object' || schema === null) { + return usedDefs; + } + + if (schema.$ref) { + const refParts = schema.$ref.split('/'); + if (refParts[0] === '#' && refParts[1] === '$defs' && refParts[2]) { + const defName = refParts[2]; + const def = defs[defName]; + if (def && !visited.has(schema.$ref)) { + usedDefs[defName] = def; + visited.add(schema.$ref); + Object.assign(usedDefs, findUsedDefs(def, defs, visited)); + visited.delete(schema.$ref); + } + } + return usedDefs; + } + + for (const key in schema) { + if (key !== '$defs' && typeof schema[key] === 'object' && schema[key] !== null) { + Object.assign(usedDefs, findUsedDefs(schema[key] as JSONSchema, defs, visited)); + } + } + + return usedDefs; +} + +// Export for testing +export { findUsedDefs }; + +/** + * Inlines all $refs in a schema, eliminating $defs. + * If a circular reference is detected, the circular property is removed. + */ +export function inlineRefs(schema: JSONSchema): JSONSchema { + if (!schema || typeof schema !== 'object') { + return schema; + } + + const clonedSchema = { ...schema }; + const defs: Record = schema.$defs || {}; + + delete clonedSchema.$defs; + + const result = inlineRefsRecursive(clonedSchema, defs, new Set()); + // The top level can never be null + return result === null ? {} : result; +} + +function inlineRefsRecursive( + schema: JSONSchema, + defs: Record, + refPath: Set, +): JSONSchema | null { + if (!schema || typeof schema !== 'object') { + return schema; + } + + if (Array.isArray(schema)) { + return schema.map((item) => { + const processed = inlineRefsRecursive(item, defs, refPath); + return processed === null ? {} : processed; + }) as JSONSchema; + } + + const result = { ...schema }; + + if ('$ref' in result && typeof result.$ref === 'string') { + if (result.$ref.startsWith('#/$defs/')) { + const refName = result.$ref.split('/').pop() as string; + const def = defs[refName]; + + // If we've already seen this ref in our path, we have a circular reference + if (refPath.has(result.$ref)) { + // For circular references, we completely remove the property + // by returning null. The parent will remove it. + return null; + } + + if (def) { + const newRefPath = new Set(refPath); + newRefPath.add(result.$ref); + + const inlinedDef = inlineRefsRecursive({ ...def }, defs, newRefPath); + + if (inlinedDef === null) { + return { ...result }; + } + + // Merge the inlined definition with the original schema's properties + // but preserve things like description, etc. + const { $ref, ...rest } = result; + return { ...inlinedDef, ...rest }; + } + } + + // Keep external refs as-is + return result; + } + + for (const key in result) { + if (result[key] && typeof result[key] === 'object') { + const processed = inlineRefsRecursive(result[key] as JSONSchema, defs, refPath); + if (processed === null) { + // Remove properties that would cause circular references + delete result[key]; + } else { + result[key] = processed; + } + } + } + + return result; +} + +/** + * Removes anyOf fields from a schema, using only the first variant. + */ +export function removeAnyOf(schema: JSONSchema): JSONSchema { + if (!schema || typeof schema !== 'object') { + return schema; + } + + if (Array.isArray(schema)) { + return schema.map((item) => removeAnyOf(item)) as JSONSchema; + } + + const result = { ...schema }; + + if ('anyOf' in result && Array.isArray(result.anyOf) && result.anyOf.length > 0) { + const firstVariant = result.anyOf[0]; + + if (firstVariant && typeof firstVariant === 'object') { + // Special handling for properties to ensure deep merge + if (firstVariant.properties && result.properties) { + result.properties = { + ...result.properties, + ...(firstVariant.properties as Record), + }; + } else if (firstVariant.properties) { + result.properties = { ...firstVariant.properties }; + } + + for (const key in firstVariant) { + if (key !== 'properties') { + result[key] = firstVariant[key]; + } + } + } + + delete result.anyOf; + } + + for (const key in result) { + if (result[key] && typeof result[key] === 'object') { + result[key] = removeAnyOf(result[key] as JSONSchema); + } + } + + return result; +} + +/** + * Removes format fields from a schema and appends them to the description. + */ +export function removeFormats(schema: JSONSchema, formatsCapability: boolean): JSONSchema { + if (formatsCapability) { + return schema; + } + + if (!schema || typeof schema !== 'object') { + return schema; + } + + if (Array.isArray(schema)) { + return schema.map((item) => removeFormats(item, formatsCapability)) as JSONSchema; + } + + const result = { ...schema }; + + if ('format' in result && typeof result['format'] === 'string') { + const formatStr = `(format: "${result['format']}")`; + + if ('description' in result && typeof result['description'] === 'string') { + result['description'] = `${result['description']} ${formatStr}`; + } else { + result['description'] = formatStr; + } + + delete result['format']; + } + + for (const key in result) { + if (result[key] && typeof result[key] === 'object') { + result[key] = removeFormats(result[key] as JSONSchema, formatsCapability); + } + } + + return result; +} + +/** + * Applies all compatibility transformations to the endpoints based on the provided capabilities. + */ +export function applyCompatibilityTransformations( + endpoints: Endpoint[], + capabilities: ClientCapabilities, +): Endpoint[] { + let transformedEndpoints = [...endpoints]; + + // Handle top-level unions first as this changes tool names + if (!capabilities.topLevelUnions) { + const newEndpoints: Endpoint[] = []; + + for (const endpoint of transformedEndpoints) { + const variantTools = removeTopLevelUnions(endpoint.tool); + + if (variantTools.length === 1) { + newEndpoints.push(endpoint); + } else { + for (const variantTool of variantTools) { + newEndpoints.push({ + ...endpoint, + tool: variantTool, + }); + } + } + } + + transformedEndpoints = newEndpoints; + } + + if (capabilities.toolNameLength) { + const toolNames = transformedEndpoints.map((endpoint) => endpoint.tool.name); + const renameMap = truncateToolNames(toolNames, capabilities.toolNameLength); + + transformedEndpoints = transformedEndpoints.map((endpoint) => ({ + ...endpoint, + tool: { + ...endpoint.tool, + name: renameMap.get(endpoint.tool.name) ?? endpoint.tool.name, + }, + })); + } + + if (!capabilities.refs || !capabilities.unions || !capabilities.formats) { + transformedEndpoints = transformedEndpoints.map((endpoint) => { + let schema = endpoint.tool.inputSchema as JSONSchema; + + if (!capabilities.refs) { + schema = inlineRefs(schema); + } + + if (!capabilities.unions) { + schema = removeAnyOf(schema); + } + + if (!capabilities.formats) { + schema = removeFormats(schema, capabilities.formats); + } + + return { + ...endpoint, + tool: { + ...endpoint.tool, + inputSchema: schema as typeof endpoint.tool.inputSchema, + }, + }; + }); + } + + return transformedEndpoints; +} + +function toSnakeCase(str: string): string { + return str + .replace(/\s+/g, '_') + .replace(/([a-z])([A-Z])/g, '$1_$2') + .toLowerCase(); +} diff --git a/packages/mcp-server/src/dynamic-tools.ts b/packages/mcp-server/src/dynamic-tools.ts new file mode 100644 index 0000000..f0d7086 --- /dev/null +++ b/packages/mcp-server/src/dynamic-tools.ts @@ -0,0 +1,159 @@ +import BeeperDesktop from 'desktop-api-typescript'; +import { Endpoint, asTextContentResult, ToolCallResult } from './tools/types'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { z } from 'zod'; +import { Cabidela } from '@cloudflare/cabidela'; + +function zodToInputSchema(schema: z.ZodSchema) { + return { + type: 'object' as const, + ...(zodToJsonSchema(schema) as any), + }; +} + +/** + * A list of tools that expose all the endpoints in the API dynamically. + * + * Instead of exposing every endpoint as it's own tool, which uses up too many tokens for LLMs to use at once, + * we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then + * a generic endpoint that can be used to invoke any endpoint with the provided arguments. + * + * @param endpoints - The endpoints to include in the list. + */ +export function dynamicTools(endpoints: Endpoint[]): Endpoint[] { + const listEndpointsSchema = z.object({ + search_query: z + .string() + .optional() + .describe( + 'An optional search query to filter the endpoints by. Provide a partial name, resource, operation, or tag to filter the endpoints returned.', + ), + }); + + const listEndpointsTool = { + metadata: { + resource: 'dynamic_tools', + operation: 'read' as const, + tags: [], + }, + tool: { + name: 'list_api_endpoints', + description: 'List or search for all endpoints in the Beeper Desktop TypeScript API', + inputSchema: zodToInputSchema(listEndpointsSchema), + }, + handler: async ( + client: BeeperDesktop, + args: Record | undefined, + ): Promise => { + const query = args && listEndpointsSchema.parse(args).search_query?.trim(); + + const filteredEndpoints = + query && query.length > 0 ? + endpoints.filter((endpoint) => { + const fieldsToMatch = [ + endpoint.tool.name, + endpoint.tool.description, + endpoint.metadata.resource, + endpoint.metadata.operation, + ...endpoint.metadata.tags, + ]; + return fieldsToMatch.some((field) => field && field.toLowerCase().includes(query.toLowerCase())); + }) + : endpoints; + + return asTextContentResult({ + tools: filteredEndpoints.map(({ tool, metadata }) => ({ + name: tool.name, + description: tool.description, + resource: metadata.resource, + operation: metadata.operation, + tags: metadata.tags, + })), + }); + }, + }; + + const getEndpointSchema = z.object({ + endpoint: z.string().describe('The name of the endpoint to get the schema for.'), + }); + const getEndpointTool = { + metadata: { + resource: 'dynamic_tools', + operation: 'read' as const, + tags: [], + }, + tool: { + name: 'get_api_endpoint_schema', + description: + 'Get the schema for an endpoint in the Beeper Desktop TypeScript API. You can use the schema returned by this tool to invoke an endpoint with the `invoke_api_endpoint` tool.', + inputSchema: zodToInputSchema(getEndpointSchema), + }, + handler: async (client: BeeperDesktop, args: Record | undefined) => { + if (!args) { + throw new Error('No endpoint provided'); + } + const endpointName = getEndpointSchema.parse(args).endpoint; + + const endpoint = endpoints.find((e) => e.tool.name === endpointName); + if (!endpoint) { + throw new Error(`Endpoint ${endpointName} not found`); + } + return asTextContentResult(endpoint.tool); + }, + }; + + const invokeEndpointSchema = z.object({ + endpoint_name: z.string().describe('The name of the endpoint to invoke.'), + args: z + .record(z.string(), z.any()) + .describe( + 'The arguments to pass to the endpoint. This must match the schema returned by the `get_api_endpoint_schema` tool.', + ), + }); + + const invokeEndpointTool = { + metadata: { + resource: 'dynamic_tools', + operation: 'write' as const, + tags: [], + }, + tool: { + name: 'invoke_api_endpoint', + description: + 'Invoke an endpoint in the Beeper Desktop TypeScript API. Note: use the `list_api_endpoints` tool to get the list of endpoints and `get_api_endpoint_schema` tool to get the schema for an endpoint.', + inputSchema: zodToInputSchema(invokeEndpointSchema), + }, + handler: async ( + client: BeeperDesktop, + args: Record | undefined, + ): Promise => { + if (!args) { + throw new Error('No endpoint provided'); + } + const { success, data, error } = invokeEndpointSchema.safeParse(args); + if (!success) { + throw new Error(`Invalid arguments for endpoint. ${error?.format()}`); + } + const { endpoint_name, args: endpointArgs } = data; + + const endpoint = endpoints.find((e) => e.tool.name === endpoint_name); + if (!endpoint) { + throw new Error( + `Endpoint ${endpoint_name} not found. Use the \`list_api_endpoints\` tool to get the list of available endpoints.`, + ); + } + + try { + // Try to validate the arguments for a better error message + const cabidela = new Cabidela(endpoint.tool.inputSchema, { fullErrors: true }); + cabidela.validate(endpointArgs); + } catch (error) { + throw new Error(`Invalid arguments for endpoint ${endpoint_name}:\n${error}`); + } + + return await endpoint.handler(client, endpointArgs); + }, + }; + + return [getEndpointTool, listEndpointsTool, invokeEndpointTool]; +} diff --git a/packages/mcp-server/src/filtering.ts b/packages/mcp-server/src/filtering.ts new file mode 100644 index 0000000..1aa9a40 --- /dev/null +++ b/packages/mcp-server/src/filtering.ts @@ -0,0 +1,14 @@ +// @ts-nocheck +import initJq from 'jq-web'; + +export async function maybeFilter(jqFilter: unknown | undefined, response: any): Promise { + if (jqFilter && typeof jqFilter === 'string') { + return await jq(response, jqFilter); + } else { + return response; + } +} + +async function jq(json: any, jqFilter: string) { + return (await initJq).json(json, jqFilter); +} diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/headers.ts new file mode 100644 index 0000000..bb29209 --- /dev/null +++ b/packages/mcp-server/src/headers.ts @@ -0,0 +1,23 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { IncomingMessage } from 'node:http'; +import { ClientOptions } from 'desktop-api-typescript'; + +export const parseAuthHeaders = (req: IncomingMessage): Partial => { + if (req.headers.authorization) { + const scheme = req.headers.authorization.split(' ')[0]!; + const value = req.headers.authorization.slice(scheme.length + 1); + switch (scheme) { + case 'Bearer': + return { accessToken: req.headers.authorization.slice('Bearer '.length) }; + default: + throw new Error(`Unsupported authorization scheme`); + } + } + + const accessToken = + Array.isArray(req.headers['x-beeper-access-token']) ? + req.headers['x-beeper-access-token'][0] + : req.headers['x-beeper-access-token']; + return { accessToken }; +}; diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts new file mode 100644 index 0000000..c11185b --- /dev/null +++ b/packages/mcp-server/src/http.ts @@ -0,0 +1,115 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; + +import express from 'express'; +import { fromError } from 'zod-validation-error/v3'; +import { McpOptions, parseQueryOptions } from './options'; +import { initMcpServer, newMcpServer } from './server'; +import { parseAuthHeaders } from './headers'; + +const newServer = ( + defaultMcpOptions: McpOptions, + req: express.Request, + res: express.Response, +): McpServer | null => { + const server = newMcpServer(); + + let mcpOptions: McpOptions; + try { + mcpOptions = parseQueryOptions(defaultMcpOptions, req.query); + } catch (error) { + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: `Invalid request: ${fromError(error)}`, + }, + }); + return null; + } + + try { + const authOptions = parseAuthHeaders(req); + initMcpServer({ + server: server, + clientOptions: { + ...authOptions, + defaultHeaders: { + 'X-Stainless-MCP': 'true', + }, + }, + mcpOptions, + }); + } catch { + res.status(401).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Unauthorized', + }, + }); + return null; + } + + return server; +}; + +const post = (defaultOptions: McpOptions) => async (req: express.Request, res: express.Response) => { + const server = newServer(defaultOptions, req, res); + // If we return null, we already set the authorization error. + if (server === null) return; + const transport = new StreamableHTTPServerTransport({ + // Stateless server + sessionIdGenerator: undefined, + }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}; + +const get = async (req: express.Request, res: express.Response) => { + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not supported', + }, + }); +}; + +const del = async (req: express.Request, res: express.Response) => { + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not supported', + }, + }); +}; + +export const streamableHTTPApp = (options: McpOptions): express.Express => { + const app = express(); + app.set('query parser', 'extended'); + app.use(express.json()); + + app.get('/', get); + app.post('/', post(options)); + app.delete('/', del); + + return app; +}; + +export const launchStreamableHTTPServer = async (options: McpOptions, port: number | string | undefined) => { + const app = streamableHTTPApp(options); + const server = app.listen(port); + const address = server.address(); + + if (typeof address === 'string') { + console.error(`MCP Server running on streamable HTTP at ${address}`); + } else if (address !== null) { + console.error(`MCP Server running on streamable HTTP on port ${address.port}`); + } else { + console.error(`MCP Server running on streamable HTTP on port ${port}`); + } +}; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts new file mode 100644 index 0000000..c450e4b --- /dev/null +++ b/packages/mcp-server/src/index.ts @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +import { selectTools } from './server'; +import { Endpoint, endpoints } from './tools'; +import { McpOptions, parseCLIOptions } from './options'; +import { launchStdioServer } from './stdio'; +import { launchStreamableHTTPServer } from './http'; + +async function main() { + const options = parseOptionsOrError(); + + if (options.list) { + listAllTools(); + return; + } + + const selectedTools = selectToolsOrError(endpoints, options); + + console.error( + `MCP Server starting with ${selectedTools.length} tools:`, + selectedTools.map((e) => e.tool.name), + ); + + switch (options.transport) { + case 'stdio': + await launchStdioServer(options); + break; + case 'http': + await launchStreamableHTTPServer(options, options.port ?? options.socket); + break; + } +} + +if (require.main === module) { + main().catch((error) => { + console.error('Fatal error in main():', error); + process.exit(1); + }); +} + +function parseOptionsOrError() { + try { + return parseCLIOptions(); + } catch (error) { + console.error('Error parsing options:', error); + process.exit(1); + } +} + +function selectToolsOrError(endpoints: Endpoint[], options: McpOptions): Endpoint[] { + try { + const includedTools = selectTools(endpoints, options); + if (includedTools.length === 0) { + console.error('No tools match the provided filters.'); + process.exit(1); + } + return includedTools; + } catch (error) { + if (error instanceof Error) { + console.error('Error filtering tools:', error.message); + } else { + console.error('Error filtering tools:', error); + } + process.exit(1); + } +} + +function listAllTools() { + if (endpoints.length === 0) { + console.log('No tools available.'); + return; + } + console.log('Available tools:\n'); + + // Group endpoints by resource + const resourceGroups = new Map(); + + for (const endpoint of endpoints) { + const resource = endpoint.metadata.resource; + if (!resourceGroups.has(resource)) { + resourceGroups.set(resource, []); + } + resourceGroups.get(resource)!.push(endpoint); + } + + // Sort resources alphabetically + const sortedResources = Array.from(resourceGroups.keys()).sort(); + + // Display hierarchically by resource + for (const resource of sortedResources) { + console.log(`Resource: ${resource}`); + + const resourceEndpoints = resourceGroups.get(resource)!; + // Sort endpoints by tool name + resourceEndpoints.sort((a, b) => a.tool.name.localeCompare(b.tool.name)); + + for (const endpoint of resourceEndpoints) { + const { + tool, + metadata: { operation, tags }, + } = endpoint; + + console.log(` - ${tool.name} (${operation}) ${tags.length > 0 ? `tags: ${tags.join(', ')}` : ''}`); + console.log(` Description: ${tool.description}`); + } + console.log(''); + } +} diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts new file mode 100644 index 0000000..2100cf5 --- /dev/null +++ b/packages/mcp-server/src/options.ts @@ -0,0 +1,456 @@ +import qs from 'qs'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import z from 'zod'; +import { endpoints, Filter } from './tools'; +import { ClientCapabilities, knownClients, ClientType } from './compat'; + +export type CLIOptions = McpOptions & { + list: boolean; + transport: 'stdio' | 'http'; + port: number | undefined; + socket: string | undefined; +}; + +export type McpOptions = { + client?: ClientType | undefined; + includeDynamicTools?: boolean | undefined; + includeAllTools?: boolean | undefined; + includeCodeTools?: boolean | undefined; + filters?: Filter[] | undefined; + capabilities?: Partial | undefined; +}; + +const CAPABILITY_CHOICES = [ + 'top-level-unions', + 'valid-json', + 'refs', + 'unions', + 'formats', + 'tool-name-length', +] as const; + +type Capability = (typeof CAPABILITY_CHOICES)[number]; + +function parseCapabilityValue(cap: string): { name: Capability; value?: number } { + if (cap.startsWith('tool-name-length=')) { + const parts = cap.split('='); + if (parts.length === 2) { + const length = parseInt(parts[1]!, 10); + if (!isNaN(length)) { + return { name: 'tool-name-length', value: length }; + } + throw new Error(`Invalid tool-name-length value: ${parts[1]}. Expected a number.`); + } + throw new Error(`Invalid format for tool-name-length. Expected tool-name-length=N.`); + } + if (!CAPABILITY_CHOICES.includes(cap as Capability)) { + throw new Error(`Unknown capability: ${cap}. Valid capabilities are: ${CAPABILITY_CHOICES.join(', ')}`); + } + return { name: cap as Capability }; +} + +export function parseCLIOptions(): CLIOptions { + const opts = yargs(hideBin(process.argv)) + .option('tools', { + type: 'string', + array: true, + choices: ['dynamic', 'all', 'code'], + description: 'Use dynamic tools or all tools', + }) + .option('no-tools', { + type: 'string', + array: true, + choices: ['dynamic', 'all', 'code'], + description: 'Do not use any dynamic or all tools', + }) + .option('tool', { + type: 'string', + array: true, + description: 'Include tools matching the specified names', + }) + .option('resource', { + type: 'string', + array: true, + description: 'Include tools matching the specified resources', + }) + .option('operation', { + type: 'string', + array: true, + choices: ['read', 'write'], + description: 'Include tools matching the specified operations', + }) + .option('tag', { + type: 'string', + array: true, + description: 'Include tools with the specified tags', + }) + .option('no-tool', { + type: 'string', + array: true, + description: 'Exclude tools matching the specified names', + }) + .option('no-resource', { + type: 'string', + array: true, + description: 'Exclude tools matching the specified resources', + }) + .option('no-operation', { + type: 'string', + array: true, + description: 'Exclude tools matching the specified operations', + }) + .option('no-tag', { + type: 'string', + array: true, + description: 'Exclude tools with the specified tags', + }) + .option('list', { + type: 'boolean', + description: 'List all tools and exit', + }) + .option('client', { + type: 'string', + choices: Object.keys(knownClients), + description: 'Specify the MCP client being used', + }) + .option('capability', { + type: 'string', + array: true, + description: 'Specify client capabilities', + coerce: (values: string[]) => { + return values.flatMap((v) => v.split(',')); + }, + }) + .option('no-capability', { + type: 'string', + array: true, + description: 'Unset client capabilities', + choices: CAPABILITY_CHOICES, + coerce: (values: string[]) => { + return values.flatMap((v) => v.split(',')); + }, + }) + .option('describe-capabilities', { + type: 'boolean', + description: 'Print detailed explanation of client capabilities and exit', + }) + .option('transport', { + type: 'string', + choices: ['stdio', 'http'], + default: 'stdio', + description: 'What transport to use; stdio for local servers or http for remote servers', + }) + .option('port', { + type: 'number', + description: 'Port to serve on if using http transport', + }) + .option('socket', { + type: 'string', + description: 'Unix socket to serve on if using http transport', + }) + .help(); + + for (const [command, desc] of examples()) { + opts.example(command, desc); + } + + const argv = opts.parseSync(); + + // Handle describe-capabilities flag + if (argv.describeCapabilities) { + console.log(getCapabilitiesExplanation()); + process.exit(0); + } + + const filters: Filter[] = []; + + // Helper function to support comma-separated values + const splitValues = (values: string[] | undefined): string[] => { + if (!values) return []; + return values.flatMap((v) => v.split(',')); + }; + + for (const tag of splitValues(argv.tag)) { + filters.push({ type: 'tag', op: 'include', value: tag }); + } + + for (const tag of splitValues(argv.noTag)) { + filters.push({ type: 'tag', op: 'exclude', value: tag }); + } + + for (const resource of splitValues(argv.resource)) { + filters.push({ type: 'resource', op: 'include', value: resource }); + } + + for (const resource of splitValues(argv.noResource)) { + filters.push({ type: 'resource', op: 'exclude', value: resource }); + } + + for (const tool of splitValues(argv.tool)) { + filters.push({ type: 'tool', op: 'include', value: tool }); + } + + for (const tool of splitValues(argv.noTool)) { + filters.push({ type: 'tool', op: 'exclude', value: tool }); + } + + for (const operation of splitValues(argv.operation)) { + filters.push({ type: 'operation', op: 'include', value: operation }); + } + + for (const operation of splitValues(argv.noOperation)) { + filters.push({ type: 'operation', op: 'exclude', value: operation }); + } + + // Parse client capabilities + const clientCapabilities: Partial = {}; + + // Apply individual capability overrides + if (Array.isArray(argv.capability)) { + for (const cap of argv.capability) { + const parsedCap = parseCapabilityValue(cap); + if (parsedCap.name === 'top-level-unions') { + clientCapabilities.topLevelUnions = true; + } else if (parsedCap.name === 'valid-json') { + clientCapabilities.validJson = true; + } else if (parsedCap.name === 'refs') { + clientCapabilities.refs = true; + } else if (parsedCap.name === 'unions') { + clientCapabilities.unions = true; + } else if (parsedCap.name === 'formats') { + clientCapabilities.formats = true; + } else if (parsedCap.name === 'tool-name-length') { + clientCapabilities.toolNameLength = parsedCap.value; + } + } + } + + // Handle no-capability options to unset capabilities + if (Array.isArray(argv.noCapability)) { + for (const cap of argv.noCapability) { + if (cap === 'top-level-unions') { + clientCapabilities.topLevelUnions = false; + } else if (cap === 'valid-json') { + clientCapabilities.validJson = false; + } else if (cap === 'refs') { + clientCapabilities.refs = false; + } else if (cap === 'unions') { + clientCapabilities.unions = false; + } else if (cap === 'formats') { + clientCapabilities.formats = false; + } else if (cap === 'tool-name-length') { + clientCapabilities.toolNameLength = undefined; + } + } + } + + const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code') => + explicitTools ? argv.tools?.includes(toolType) && !argv.noTools?.includes(toolType) : undefined; + + const explicitTools = Boolean(argv.tools || argv.noTools); + const includeDynamicTools = shouldIncludeToolType('dynamic'); + const includeAllTools = shouldIncludeToolType('all'); + const includeCodeTools = shouldIncludeToolType('code'); + + const transport = argv.transport as 'stdio' | 'http'; + + const client = argv.client as ClientType; + return { + client: client && client !== 'infer' && knownClients[client] ? client : undefined, + includeDynamicTools, + includeAllTools, + includeCodeTools, + filters, + capabilities: clientCapabilities, + list: argv.list || false, + transport, + port: argv.port, + socket: argv.socket, + }; +} + +const coerceArray = (zodType: T) => + z.preprocess( + (val) => + Array.isArray(val) ? val + : val ? [val] + : val, + z.array(zodType).optional(), + ); + +const QueryOptions = z.object({ + tools: coerceArray(z.enum(['dynamic', 'all'])).describe('Use dynamic tools or all tools'), + no_tools: coerceArray(z.enum(['dynamic', 'all'])).describe('Do not use dynamic tools or all tools'), + tool: coerceArray(z.string()).describe('Include tools matching the specified names'), + resource: coerceArray(z.string()).describe('Include tools matching the specified resources'), + operation: coerceArray(z.enum(['read', 'write'])).describe( + 'Include tools matching the specified operations', + ), + tag: coerceArray(z.string()).describe('Include tools with the specified tags'), + no_tool: coerceArray(z.string()).describe('Exclude tools matching the specified names'), + no_resource: coerceArray(z.string()).describe('Exclude tools matching the specified resources'), + no_operation: coerceArray(z.enum(['read', 'write'])).describe( + 'Exclude tools matching the specified operations', + ), + no_tag: coerceArray(z.string()).describe('Exclude tools with the specified tags'), + client: ClientType.optional().describe('Specify the MCP client being used'), + capability: coerceArray(z.string()).describe('Specify client capabilities'), + no_capability: coerceArray(z.enum(CAPABILITY_CHOICES)).describe('Unset client capabilities'), +}); + +export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): McpOptions { + const queryObject = typeof query === 'string' ? qs.parse(query) : query; + const queryOptions = QueryOptions.parse(queryObject); + + const filters: Filter[] = [...(defaultOptions.filters ?? [])]; + + for (const resource of queryOptions.resource || []) { + filters.push({ type: 'resource', op: 'include', value: resource }); + } + for (const operation of queryOptions.operation || []) { + filters.push({ type: 'operation', op: 'include', value: operation }); + } + for (const tag of queryOptions.tag || []) { + filters.push({ type: 'tag', op: 'include', value: tag }); + } + for (const tool of queryOptions.tool || []) { + filters.push({ type: 'tool', op: 'include', value: tool }); + } + for (const resource of queryOptions.no_resource || []) { + filters.push({ type: 'resource', op: 'exclude', value: resource }); + } + for (const operation of queryOptions.no_operation || []) { + filters.push({ type: 'operation', op: 'exclude', value: operation }); + } + for (const tag of queryOptions.no_tag || []) { + filters.push({ type: 'tag', op: 'exclude', value: tag }); + } + for (const tool of queryOptions.no_tool || []) { + filters.push({ type: 'tool', op: 'exclude', value: tool }); + } + + // Parse client capabilities + const clientCapabilities: Partial = { ...defaultOptions.capabilities }; + + for (const cap of queryOptions.capability || []) { + const parsed = parseCapabilityValue(cap); + if (parsed.name === 'top-level-unions') { + clientCapabilities.topLevelUnions = true; + } else if (parsed.name === 'valid-json') { + clientCapabilities.validJson = true; + } else if (parsed.name === 'refs') { + clientCapabilities.refs = true; + } else if (parsed.name === 'unions') { + clientCapabilities.unions = true; + } else if (parsed.name === 'formats') { + clientCapabilities.formats = true; + } else if (parsed.name === 'tool-name-length') { + clientCapabilities.toolNameLength = parsed.value; + } + } + + for (const cap of queryOptions.no_capability || []) { + if (cap === 'top-level-unions') { + clientCapabilities.topLevelUnions = false; + } else if (cap === 'valid-json') { + clientCapabilities.validJson = false; + } else if (cap === 'refs') { + clientCapabilities.refs = false; + } else if (cap === 'unions') { + clientCapabilities.unions = false; + } else if (cap === 'formats') { + clientCapabilities.formats = false; + } else if (cap === 'tool-name-length') { + clientCapabilities.toolNameLength = undefined; + } + } + + let dynamicTools: boolean | undefined = + queryOptions.no_tools && !queryOptions.no_tools?.includes('dynamic') ? false + : queryOptions.tools?.includes('dynamic') ? true + : defaultOptions.includeDynamicTools; + + let allTools: boolean | undefined = + queryOptions.no_tools && !queryOptions.no_tools?.includes('all') ? false + : queryOptions.tools?.includes('all') ? true + : defaultOptions.includeAllTools; + + return { + client: queryOptions.client ?? defaultOptions.client, + includeDynamicTools: dynamicTools, + includeAllTools: allTools, + includeCodeTools: undefined, + filters, + capabilities: clientCapabilities, + }; +} + +function getCapabilitiesExplanation(): string { + return ` +Client Capabilities Explanation: + +Different Language Models (LLMs) and the MCP clients that use them have varying limitations in how they handle tool schemas. Capability flags allow you to inform the MCP server about these limitations. + +When a capability flag is set to false, the MCP server will automatically adjust the tool schemas to work around that limitation, ensuring broader compatibility. + +Available Capabilities: + +# top-level-unions +Some clients/LLMs do not support JSON schemas with a union type (anyOf) at the root level. If a client lacks this capability, the MCP server splits tools with top-level unions into multiple separate tools, one for each variant in the union. + +# refs +Some clients/LLMs do not support $ref pointers for schema reuse. If a client lacks this capability, the MCP server automatically inlines all references ($defs) directly into the schema. Properties that would cause circular references are removed during this process. + +# valid-json +Some clients/LLMs may incorrectly send arguments as a JSON-encoded string instead of a proper JSON object. If a client *has* this capability, the MCP server will attempt to parse string values as JSON if the initial validation against the schema fails. + +# unions +Some clients/LLMs do not support union types (anyOf) in JSON schemas. If a client lacks this capability, the MCP server removes all anyOf fields and uses only the first variant as the schema. + +# formats +Some clients/LLMs do not support the 'format' keyword in JSON Schema specifications. If a client lacks this capability, the MCP server removes all format fields and appends the format information to the field's description in parentheses. + +# tool-name-length=N +Some clients/LLMs impose a maximum length on tool names. If this capability is set, the MCP server will automatically truncate tool names exceeding the specified length (N), ensuring uniqueness by appending numbers if necessary. + +Client Presets (--client): +Presets like '--client=openai-agents' or '--client=cursor' automatically configure these capabilities based on current known limitations of those clients, simplifying setup. + +Current presets: +${JSON.stringify(knownClients, null, 2)} + `; +} + +function examples(): [string, string][] { + const firstEndpoint = endpoints[0]!; + const secondEndpoint = + endpoints.find((e) => e.metadata.resource !== firstEndpoint.metadata.resource) || endpoints[1]; + const tag = endpoints.find((e) => e.metadata.tags.length > 0)?.metadata.tags[0]; + const otherEndpoint = secondEndpoint || firstEndpoint; + + return [ + [ + `--tool="${firstEndpoint.tool.name}" ${secondEndpoint ? `--tool="${secondEndpoint.tool.name}"` : ''}`, + 'Include tools by name', + ], + [ + `--resource="${firstEndpoint.metadata.resource}" --operation="read"`, + 'Filter by resource and operation', + ], + [ + `--resource="${otherEndpoint.metadata.resource}*" --no-tool="${otherEndpoint.tool.name}"`, + 'Use resource wildcards and exclusions', + ], + [`--client="cursor"`, 'Adjust schemas to be more compatible with Cursor'], + [ + `--capability="top-level-unions" --capability="tool-name-length=40"`, + 'Specify individual client capabilities', + ], + [ + `--client="cursor" --no-capability="tool-name-length"`, + 'Use cursor client preset but remove tool name length limit', + ], + ...(tag ? [[`--tag="${tag}"`, 'Filter based on tags'] as [string, string]] : []), + ]; +} diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts new file mode 100644 index 0000000..f738b4f --- /dev/null +++ b/packages/mcp-server/src/server.ts @@ -0,0 +1,180 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { Endpoint, endpoints, HandlerFunction, query } from './tools'; +import { + CallToolRequestSchema, + Implementation, + ListToolsRequestSchema, + Tool, +} from '@modelcontextprotocol/sdk/types.js'; +import { ClientOptions } from 'desktop-api-typescript'; +import BeeperDesktop from 'desktop-api-typescript'; +import { + applyCompatibilityTransformations, + ClientCapabilities, + defaultClientCapabilities, + knownClients, + parseEmbeddedJSON, +} from './compat'; +import { dynamicTools } from './dynamic-tools'; +import { codeTool } from './code-tool'; +import { McpOptions } from './options'; + +export { McpOptions } from './options'; +export { ClientType } from './compat'; +export { Filter } from './tools'; +export { ClientOptions } from 'desktop-api-typescript'; +export { endpoints } from './tools'; + +export const newMcpServer = () => + new McpServer( + { + name: 'desktop_api_typescript_api', + version: '0.1.0', + }, + { capabilities: { tools: {}, logging: {} } }, + ); + +// Create server instance +export const server = newMcpServer(); + +/** + * Initializes the provided MCP Server with the given tools and handlers. + * If not provided, the default client, tools and handlers will be used. + */ +export function initMcpServer(params: { + server: Server | McpServer; + clientOptions?: ClientOptions; + mcpOptions?: McpOptions; +}) { + const server = params.server instanceof McpServer ? params.server.server : params.server; + const mcpOptions = params.mcpOptions ?? {}; + + let providedEndpoints: Endpoint[] | null = null; + let endpointMap: Record | null = null; + + const initTools = (implementation?: Implementation) => { + if (implementation && (!mcpOptions.client || mcpOptions.client === 'infer')) { + mcpOptions.client = + implementation.name.toLowerCase().includes('claude') ? 'claude' + : implementation.name.toLowerCase().includes('cursor') ? 'cursor' + : undefined; + mcpOptions.capabilities = { + ...(mcpOptions.client && knownClients[mcpOptions.client]), + ...mcpOptions.capabilities, + }; + } + providedEndpoints = selectTools(endpoints, mcpOptions); + endpointMap = Object.fromEntries(providedEndpoints.map((endpoint) => [endpoint.tool.name, endpoint])); + }; + + const logAtLevel = + (level: 'debug' | 'info' | 'warning' | 'error') => + (message: string, ...rest: unknown[]) => { + void server.sendLoggingMessage({ + level, + data: { message, rest }, + }); + }; + const logger = { + debug: logAtLevel('debug'), + info: logAtLevel('info'), + warn: logAtLevel('warning'), + error: logAtLevel('error'), + }; + + const client = new BeeperDesktop({ + logger, + ...params.clientOptions, + defaultHeaders: { + ...params.clientOptions?.defaultHeaders, + 'X-Stainless-MCP': 'true', + }, + }); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + if (providedEndpoints === null) { + initTools(server.getClientVersion()); + } + return { + tools: providedEndpoints!.map((endpoint) => endpoint.tool), + }; + }); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (endpointMap === null) { + initTools(server.getClientVersion()); + } + const { name, arguments: args } = request.params; + const endpoint = endpointMap![name]; + if (!endpoint) { + throw new Error(`Unknown tool: ${name}`); + } + + return executeHandler(endpoint.tool, endpoint.handler, client, args, mcpOptions.capabilities); + }); +} + +/** + * Selects the tools to include in the MCP Server based on the provided options. + */ +export function selectTools(endpoints: Endpoint[], options?: McpOptions): Endpoint[] { + const filteredEndpoints = query(options?.filters ?? [], endpoints); + + let includedTools = filteredEndpoints; + + if (includedTools.length > 0) { + if (options?.includeDynamicTools) { + includedTools = dynamicTools(includedTools); + } + } else { + if (options?.includeAllTools) { + includedTools = endpoints; + } else if (options?.includeDynamicTools) { + includedTools = dynamicTools(endpoints); + } else if (options?.includeCodeTools) { + includedTools = [codeTool()]; + } else { + includedTools = endpoints; + } + } + + const capabilities = { ...defaultClientCapabilities, ...options?.capabilities }; + return applyCompatibilityTransformations(includedTools, capabilities); +} + +/** + * Runs the provided handler with the given client and arguments. + */ +export async function executeHandler( + tool: Tool, + handler: HandlerFunction, + client: BeeperDesktop, + args: Record | undefined, + compatibilityOptions?: Partial, +) { + const options = { ...defaultClientCapabilities, ...compatibilityOptions }; + if (!options.validJson && args) { + args = parseEmbeddedJSON(args, tool.inputSchema); + } + return await handler(client, args || {}); +} + +export const readEnv = (env: string): string | undefined => { + if (typeof (globalThis as any).process !== 'undefined') { + return (globalThis as any).process.env?.[env]?.trim(); + } else if (typeof (globalThis as any).Deno !== 'undefined') { + return (globalThis as any).Deno.env?.get?.(env)?.trim(); + } + return; +}; + +export const readEnvOrError = (env: string): string => { + let envValue = readEnv(env); + if (envValue === undefined) { + throw new Error(`Environment variable ${env} is not set`); + } + return envValue; +}; diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts new file mode 100644 index 0000000..d902a5b --- /dev/null +++ b/packages/mcp-server/src/stdio.ts @@ -0,0 +1,13 @@ +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { initMcpServer, newMcpServer } from './server'; +import { McpOptions } from './options'; + +export const launchStdioServer = async (options: McpOptions) => { + const server = newMcpServer(); + + initMcpServer({ server, mcpOptions: options }); + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('MCP Server running on stdio'); +}; diff --git a/packages/mcp-server/src/tools.ts b/packages/mcp-server/src/tools.ts new file mode 100644 index 0000000..7e516de --- /dev/null +++ b/packages/mcp-server/src/tools.ts @@ -0,0 +1 @@ +export * from './tools/index'; diff --git a/packages/mcp-server/src/tools/accounts/list-accounts.ts b/packages/mcp-server/src/tools/accounts/list-accounts.ts new file mode 100644 index 0000000..5064358 --- /dev/null +++ b/packages/mcp-server/src/tools/accounts/list-accounts.ts @@ -0,0 +1,44 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'accounts', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/v0/get-accounts', + operationId: 'get_accounts', +}; + +export const tool: Tool = { + name: 'list_accounts', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nList connected Beeper accounts available on this device.\n- When to use: select account context before account-scoped operations.\n- Scope: only accounts currently Connected on this device are included.\nReturns: connected accounts.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/accounts_response',\n $defs: {\n accounts_response: {\n type: 'object',\n description: 'Response payload for listing connected Beeper accounts.',\n properties: {\n accounts: {\n type: 'array',\n description: 'Connected accounts the user can act through. Includes accountID, network, and user identity.',\n items: {\n $ref: '#/$defs/account'\n }\n }\n },\n required: [ 'accounts'\n ]\n },\n account: {\n type: 'object',\n description: 'A chat account added to Beeper',\n properties: {\n accountID: {\n type: 'string',\n description: 'Chat account added to Beeper. Use this to route account-scoped actions.'\n },\n network: {\n type: 'string',\n description: 'Display-only human-readable network name (e.g., \\'WhatsApp\\', \\'Messenger\\'). You MUST use \\'accountID\\' to perform actions.'\n },\n user: {\n $ref: '#/$defs/user'\n }\n },\n required: [ 'accountID',\n 'network',\n 'user'\n ]\n },\n user: {\n type: 'object',\n description: 'A person on or reachable through Beeper. Values are best-effort and can vary by network.',\n properties: {\n id: {\n type: 'string',\n description: 'Stable Beeper user ID. Use as the primary key when referencing a person.'\n },\n cannotMessage: {\n type: 'boolean',\n description: 'True if Beeper cannot initiate messages to this user (e.g., blocked, network restriction, or no DM path). The user may still message you.'\n },\n email: {\n type: 'string',\n description: 'Email address if known. Not guaranteed verified.'\n },\n fullName: {\n type: 'string',\n description: 'Display name as shown in clients (e.g., \\'Alice Example\\'). May include emojis.'\n },\n imgURL: {\n type: 'string',\n description: 'Avatar image URL if available. May be temporary or local-only to this device; download promptly if durable access is needed.'\n },\n isSelf: {\n type: 'boolean',\n description: 'True if this user represents the authenticated account\\'s own identity.'\n },\n phoneNumber: {\n type: 'string',\n description: 'User\\'s phone number in E.164 format (e.g., \\'+14155552671\\'). Omit if unknown.'\n },\n username: {\n type: 'string',\n description: 'Human-readable handle if available (e.g., \\'@alice\\'). May be network-specific and not globally unique.'\n }\n },\n required: [ 'id'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.accounts.list())); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/app/focus-app.ts b/packages/mcp-server/src/tools/app/focus-app.ts new file mode 100644 index 0000000..ff6e788 --- /dev/null +++ b/packages/mcp-server/src/tools/app/focus-app.ts @@ -0,0 +1,51 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'app', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/focus-app', + operationId: 'focus_app', +}; + +export const tool: Tool = { + name: 'focus_app', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nBring Beeper Desktop to the foreground on this device. Optionally focuses a specific chat if chatID is provided.\n- When to use: open Beeper, or jump to a specific chat.\n- Constraints: requires Beeper Desktop running locally; no-op in headless environments.\n- Idempotent: safe to call repeatedly. Returns an error if chatID is not found.\nReturns: success.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/base_response',\n $defs: {\n base_response: {\n type: 'object',\n properties: {\n success: {\n type: 'boolean'\n },\n error: {\n type: 'string'\n }\n },\n required: [ 'success'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: + 'Optional Beeper chat ID to focus after bringing the app to foreground. If omitted, only foregrounds the app. Required if messageSortKey is present. No-op in headless environments.', + }, + messageSortKey: { + type: 'string', + description: 'Optional message sort key. Jumps to that message in the chat when foregrounding.', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: [], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.app.focus(body))); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/archive-chats.ts b/packages/mcp-server/src/tools/chats/archive-chats.ts new file mode 100644 index 0000000..b069270 --- /dev/null +++ b/packages/mcp-server/src/tools/chats/archive-chats.ts @@ -0,0 +1,50 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/archive-chat', + operationId: 'archive_chat', +}; + +export const tool: Tool = { + name: 'archive_chats', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nArchive or unarchive a chat. Set archived=true to move to archive, archived=false to move back to inbox\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/base_response',\n $defs: {\n base_response: {\n type: 'object',\n properties: {\n success: {\n type: 'boolean'\n },\n error: {\n type: 'string'\n }\n },\n required: [ 'success'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'The identifier of the chat to archive or unarchive', + }, + archived: { + type: 'boolean', + description: 'True to archive, false to unarchive', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: ['chatID'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.chats.archive(body))); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/find-chats.ts b/packages/mcp-server/src/tools/chats/find-chats.ts new file mode 100644 index 0000000..27b6514 --- /dev/null +++ b/packages/mcp-server/src/tools/chats/find-chats.ts @@ -0,0 +1,109 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/v0/find-chats', + operationId: 'find_chats', +}; + +export const tool: Tool = { + name: 'find_chats', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nSearch and filter conversations across all messaging accounts.\n- When to use: browse chats by inbox (primary/low-priority/archive), type, unread status, or search terms.\n- Pagination: use cursor + direction for pagination.\n- Performance: provide accountIDs when known for faster filtering.\nReturns: matching chats with pagination.\nAgents: ALWAYS use linkToChat to make clickable links in your response\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/find_chats_response',\n $defs: {\n find_chats_response: {\n type: 'object',\n properties: {\n data: {\n type: 'array',\n description: 'Chats matching the filters.',\n items: {\n $ref: '#/$defs/chat'\n }\n },\n has_more: {\n type: 'boolean',\n description: 'Whether there are more items available after this set.'\n }\n },\n required: [ 'data',\n 'has_more'\n ]\n },\n chat: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier for cursor pagination.'\n },\n accountID: {\n type: 'string',\n description: 'Beeper account ID this chat belongs to.'\n },\n chatID: {\n type: 'string',\n description: 'Unique identifier of the chat (room/thread ID, same as id).'\n },\n network: {\n type: 'string',\n description: 'Display-only human-readable network name (e.g., \\'WhatsApp\\', \\'Messenger\\'). You MUST use \\'accountID\\' to perform actions.'\n },\n participants: {\n type: 'object',\n description: 'Chat participants information.',\n properties: {\n hasMore: {\n type: 'boolean',\n description: 'True if there are more participants than included in items.'\n },\n items: {\n type: 'array',\n description: 'Participants returned for this chat (limited by the request; may be a subset).',\n items: {\n $ref: '#/$defs/user'\n }\n },\n total: {\n type: 'integer',\n description: 'Total number of participants in the chat.'\n }\n },\n required: [ 'hasMore',\n 'items',\n 'total'\n ]\n },\n title: {\n type: 'string',\n description: 'Display title of the chat as computed by the client/server.'\n },\n type: {\n type: 'string',\n description: 'Chat type: \\'single\\' for direct messages, \\'group\\' for group chats, \\'channel\\' for channels, \\'broadcast\\' for broadcasts.',\n enum: [ 'single',\n 'group',\n 'channel',\n 'broadcast'\n ]\n },\n unreadCount: {\n type: 'integer',\n description: 'Number of unread messages.'\n },\n isArchived: {\n type: 'boolean',\n description: 'True if chat is archived.'\n },\n isMuted: {\n type: 'boolean',\n description: 'True if chat notifications are muted.'\n },\n isPinned: {\n type: 'boolean',\n description: 'True if chat is pinned.'\n },\n lastActivity: {\n type: 'string',\n description: 'Timestamp of last activity. Chats with more recent activity are often more important.',\n format: 'date-time'\n },\n lastReadMessageSortKey: {\n anyOf: [ {\n type: 'integer'\n },\n {\n type: 'string'\n }\n ],\n description: 'Last read message sortKey (hsOrder). Used to compute \\'isUnread\\'.'\n },\n linkToChat: {\n type: 'string',\n description: 'Deep link to open this chat in Beeper. AI agents should ALWAYS include this as a clickable link in responses.'\n }\n },\n required: [ 'id',\n 'accountID',\n 'chatID',\n 'network',\n 'participants',\n 'title',\n 'type',\n 'unreadCount'\n ]\n },\n user: {\n type: 'object',\n description: 'A person on or reachable through Beeper. Values are best-effort and can vary by network.',\n properties: {\n id: {\n type: 'string',\n description: 'Stable Beeper user ID. Use as the primary key when referencing a person.'\n },\n cannotMessage: {\n type: 'boolean',\n description: 'True if Beeper cannot initiate messages to this user (e.g., blocked, network restriction, or no DM path). The user may still message you.'\n },\n email: {\n type: 'string',\n description: 'Email address if known. Not guaranteed verified.'\n },\n fullName: {\n type: 'string',\n description: 'Display name as shown in clients (e.g., \\'Alice Example\\'). May include emojis.'\n },\n imgURL: {\n type: 'string',\n description: 'Avatar image URL if available. May be temporary or local-only to this device; download promptly if durable access is needed.'\n },\n isSelf: {\n type: 'boolean',\n description: 'True if this user represents the authenticated account\\'s own identity.'\n },\n phoneNumber: {\n type: 'string',\n description: 'User\\'s phone number in E.164 format (e.g., \\'+14155552671\\'). Omit if unknown.'\n },\n username: {\n type: 'string',\n description: 'Human-readable handle if available (e.g., \\'@alice\\'). May be network-specific and not globally unique.'\n }\n },\n required: [ 'id'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + accountIDs: { + type: 'array', + description: 'Provide an array of account IDs to filter chats from specific messaging accounts only', + items: { + type: 'string', + }, + }, + ending_before: { + type: 'string', + description: + 'A cursor for use in pagination. ending_before is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_bar, your subsequent call can include ending_before=obj_bar in order to fetch the previous page of the list.', + }, + inbox: { + type: 'string', + description: + 'Filter by inbox type: "primary" (non-archived, non-low-priority), "low-priority", or "archive". If not specified, shows all chats.', + enum: ['primary', 'low-priority', 'archive'], + }, + includeMuted: { + type: 'boolean', + description: + 'Include chats marked as Muted by the user, which are usually less important. Default: true. Set to false if the user wants a more refined search.', + }, + lastActivityAfter: { + type: 'string', + description: + 'Provide an ISO datetime string to only retrieve chats with last activity after this time', + format: 'date-time', + }, + lastActivityBefore: { + type: 'string', + description: + 'Provide an ISO datetime string to only retrieve chats with last activity before this time', + format: 'date-time', + }, + limit: { + type: 'integer', + description: 'Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50', + }, + participantQuery: { + type: 'string', + description: + 'Search string to filter chats by participant names. When multiple words provided, ALL words must match. Searches in username, displayName, and fullName fields.', + }, + query: { + type: 'string', + description: + 'Search string to filter chats by title. When multiple words provided, ALL words must match. Matches are case-insensitive substrings.', + }, + starting_after: { + type: 'string', + description: + 'A cursor for use in pagination. starting_after is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include starting_after=obj_foo in order to fetch the next page of the list.', + }, + type: { + type: 'string', + description: + 'Specify the type of chats to retrieve: use "single" for direct messages, "group" for group chats, "channel" for channels, or "any" to get all types', + enum: ['single', 'group', 'channel', 'any'], + }, + unreadOnly: { + type: 'boolean', + description: 'Set to true to only retrieve chats that have unread messages', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + const response = await client.chats.find(body).asResponse(); + return asTextContentResult(await maybeFilter(jq_filter, await response.json())); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/get-link-chats.ts b/packages/mcp-server/src/tools/chats/get-link-chats.ts new file mode 100644 index 0000000..ec8be5f --- /dev/null +++ b/packages/mcp-server/src/tools/chats/get-link-chats.ts @@ -0,0 +1,50 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/get-link-to-chat', + operationId: 'get_link_to_chat', +}; + +export const tool: Tool = { + name: 'get_link_chats', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nGenerate a deep link to a specific chat or message. This link can be used to open the chat directly in the Beeper app.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/link_response',\n $defs: {\n link_response: {\n type: 'object',\n description: 'URL to open a specific chat or message.',\n properties: {\n url: {\n type: 'string',\n description: 'Deep link URL to the specified chat or message.'\n }\n },\n required: [ 'url'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'The ID of the chat to link to.', + }, + messageSortKey: { + type: 'string', + description: 'Optional message sort key. Jumps to that message in the chat.', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: ['chatID'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.chats.getLink(body))); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/retrieve-chats.ts b/packages/mcp-server/src/tools/chats/retrieve-chats.ts new file mode 100644 index 0000000..d7109d1 --- /dev/null +++ b/packages/mcp-server/src/tools/chats/retrieve-chats.ts @@ -0,0 +1,54 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/v0/get-chat', + operationId: 'get_chat', +}; + +export const tool: Tool = { + name: 'retrieve_chats', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nRetrieve chat details: metadata, participants (limited), and latest message.\n- When to use: fetch a complete view of a chat beyond what search returns.\n- Constraints: not available for iMessage chats ('imsg##'). Participants limited by 'maxParticipantCount' (default 20, max 500).\nReturns: chat details.Agents: ALWAYS use linkToChat to make clickable links in your response\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/get_chat_response',\n $defs: {\n get_chat_response: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier for cursor pagination.'\n },\n accountID: {\n type: 'string',\n description: 'Beeper account ID this chat belongs to.'\n },\n chatID: {\n type: 'string',\n description: 'Unique identifier of the chat (room/thread ID, same as id).'\n },\n network: {\n type: 'string',\n description: 'Display-only human-readable network name (e.g., \\'WhatsApp\\', \\'Messenger\\'). You MUST use \\'accountID\\' to perform actions.'\n },\n participants: {\n type: 'object',\n description: 'Chat participants information.',\n properties: {\n hasMore: {\n type: 'boolean',\n description: 'True if there are more participants than included in items.'\n },\n items: {\n type: 'array',\n description: 'Participants returned for this chat (limited by the request; may be a subset).',\n items: {\n $ref: '#/$defs/user'\n }\n },\n total: {\n type: 'integer',\n description: 'Total number of participants in the chat.'\n }\n },\n required: [ 'hasMore',\n 'items',\n 'total'\n ]\n },\n title: {\n type: 'string',\n description: 'Display title of the chat as computed by the client/server.'\n },\n type: {\n type: 'string',\n description: 'Chat type: \\'single\\' for direct messages, \\'group\\' for group chats, \\'channel\\' for channels, \\'broadcast\\' for broadcasts.',\n enum: [ 'single',\n 'group',\n 'channel',\n 'broadcast'\n ]\n },\n unreadCount: {\n type: 'integer',\n description: 'Number of unread messages.'\n },\n isArchived: {\n type: 'boolean',\n description: 'True if chat is archived.'\n },\n isMuted: {\n type: 'boolean',\n description: 'True if chat notifications are muted.'\n },\n isPinned: {\n type: 'boolean',\n description: 'True if chat is pinned.'\n },\n lastActivity: {\n type: 'string',\n description: 'Timestamp of last activity. Chats with more recent activity are often more important.',\n format: 'date-time'\n },\n lastReadMessageSortKey: {\n anyOf: [ {\n type: 'integer'\n },\n {\n type: 'string'\n }\n ],\n description: 'Last read message sortKey (hsOrder). Used to compute \\'isUnread\\'.'\n },\n linkToChat: {\n type: 'string',\n description: 'Deep link to open this chat in Beeper. AI agents should ALWAYS include this as a clickable link in responses.'\n }\n },\n required: [ 'id',\n 'accountID',\n 'chatID',\n 'network',\n 'participants',\n 'title',\n 'type',\n 'unreadCount'\n ]\n },\n user: {\n type: 'object',\n description: 'A person on or reachable through Beeper. Values are best-effort and can vary by network.',\n properties: {\n id: {\n type: 'string',\n description: 'Stable Beeper user ID. Use as the primary key when referencing a person.'\n },\n cannotMessage: {\n type: 'boolean',\n description: 'True if Beeper cannot initiate messages to this user (e.g., blocked, network restriction, or no DM path). The user may still message you.'\n },\n email: {\n type: 'string',\n description: 'Email address if known. Not guaranteed verified.'\n },\n fullName: {\n type: 'string',\n description: 'Display name as shown in clients (e.g., \\'Alice Example\\'). May include emojis.'\n },\n imgURL: {\n type: 'string',\n description: 'Avatar image URL if available. May be temporary or local-only to this device; download promptly if durable access is needed.'\n },\n isSelf: {\n type: 'boolean',\n description: 'True if this user represents the authenticated account\\'s own identity.'\n },\n phoneNumber: {\n type: 'string',\n description: 'User\\'s phone number in E.164 format (e.g., \\'+14155552671\\'). Omit if unknown.'\n },\n username: {\n type: 'string',\n description: 'Human-readable handle if available (e.g., \\'@alice\\'). May be network-specific and not globally unique.'\n }\n },\n required: [ 'id'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: + "Unique identifier of the chat to retrieve. Not available for iMessage chats. Participants are limited by 'maxParticipantCount'.", + }, + maxParticipantCount: { + type: 'integer', + description: + 'Maximum number of participants to return. Use -1 for all; otherwise 0–500. Defaults to 20.', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: ['chatID'], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.chats.retrieve(body))); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts new file mode 100644 index 0000000..a7be1f9 --- /dev/null +++ b/packages/mcp-server/src/tools/index.ts @@ -0,0 +1,95 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, Endpoint, HandlerFunction } from './types'; + +export { Metadata, Endpoint, HandlerFunction }; + +import list_accounts from './accounts/list-accounts'; +import focus_app from './app/focus-app'; +import draft_messages from './messages/draft-messages'; +import search_messages from './messages/search-messages'; +import send_messages from './messages/send-messages'; +import retrieve_chats from './chats/retrieve-chats'; +import archive_chats from './chats/archive-chats'; +import find_chats from './chats/find-chats'; +import get_link_chats from './chats/get-link-chats'; +import clear_reminders from './reminders/clear-reminders'; +import set_reminders from './reminders/set-reminders'; +import get_user_info_oauth from './oauth/get-user-info-oauth'; +import revoke_token_oauth from './oauth/revoke-token-oauth'; + +export const endpoints: Endpoint[] = []; + +function addEndpoint(endpoint: Endpoint) { + endpoints.push(endpoint); +} + +addEndpoint(list_accounts); +addEndpoint(focus_app); +addEndpoint(draft_messages); +addEndpoint(search_messages); +addEndpoint(send_messages); +addEndpoint(retrieve_chats); +addEndpoint(archive_chats); +addEndpoint(find_chats); +addEndpoint(get_link_chats); +addEndpoint(clear_reminders); +addEndpoint(set_reminders); +addEndpoint(get_user_info_oauth); +addEndpoint(revoke_token_oauth); + +export type Filter = { + type: 'resource' | 'operation' | 'tag' | 'tool'; + op: 'include' | 'exclude'; + value: string; +}; + +export function query(filters: Filter[], endpoints: Endpoint[]): Endpoint[] { + const allExcludes = filters.length > 0 && filters.every((filter) => filter.op === 'exclude'); + const unmatchedFilters = new Set(filters); + + const filtered = endpoints.filter((endpoint: Endpoint) => { + let included = false || allExcludes; + + for (const filter of filters) { + if (match(filter, endpoint)) { + unmatchedFilters.delete(filter); + included = filter.op === 'include'; + } + } + + return included; + }); + + // Check if any filters didn't match + const unmatched = Array.from(unmatchedFilters).filter((f) => f.type === 'tool' || f.type === 'resource'); + if (unmatched.length > 0) { + throw new Error( + `The following filters did not match any endpoints: ${unmatched + .map((f) => `${f.type}=${f.value}`) + .join(', ')}`, + ); + } + + return filtered; +} + +function match({ type, value }: Filter, endpoint: Endpoint): boolean { + switch (type) { + case 'resource': { + const regexStr = '^' + normalizeResource(value).replace(/\*/g, '.*') + '$'; + const regex = new RegExp(regexStr); + return regex.test(normalizeResource(endpoint.metadata.resource)); + } + case 'operation': + return endpoint.metadata.operation === value; + case 'tag': + return endpoint.metadata.tags.includes(value); + case 'tool': + return endpoint.tool.name === value; + } +} + +function normalizeResource(resource: string): string { + return resource.toLowerCase().replace(/[^a-z.*\-_]*/g, ''); +} diff --git a/packages/mcp-server/src/tools/messages/draft-messages.ts b/packages/mcp-server/src/tools/messages/draft-messages.ts new file mode 100644 index 0000000..4ba9479 --- /dev/null +++ b/packages/mcp-server/src/tools/messages/draft-messages.ts @@ -0,0 +1,56 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'messages', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/draft-message', + operationId: 'draft_message', +}; + +export const tool: Tool = { + name: 'draft_messages', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nDraft a message in a specific chat. This will be placed in the message input field without sending\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/base_response',\n $defs: {\n base_response: {\n type: 'object',\n properties: {\n success: {\n type: 'boolean'\n },\n error: {\n type: 'string'\n }\n },\n required: [ 'success'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'Provide the unique identifier of the chat where you want to draft a message', + }, + focusApp: { + type: 'boolean', + description: + 'Set to true to bring Beeper application to the foreground, or false to draft silently in background', + }, + text: { + type: 'string', + description: + 'Provide the text content you want to draft. This will be placed in the message input field without sending', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: ['chatID'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.messages.draft(body))); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/messages/search-messages.ts b/packages/mcp-server/src/tools/messages/search-messages.ts new file mode 100644 index 0000000..12daa88 --- /dev/null +++ b/packages/mcp-server/src/tools/messages/search-messages.ts @@ -0,0 +1,133 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'messages', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/v0/search-messages', + operationId: 'search_messages', +}; + +export const tool: Tool = { + name: 'search_messages', + description: + 'Search messages across chats using Beeper\'s message index.\n- When to use: find messages by text and/or filters (chatIDs, accountIDs, chatType, media type filters, sender, date ranges).\n- CRITICAL: Query is LITERAL WORD MATCHING, NOT semantic search! Only finds messages containing these EXACT words.\n • ✅ RIGHT: query="dinner" or query="sick" or query="error" (single words users type)\n • ❌ WRONG: query="dinner plans tonight" or query="health issues" (phrases/concepts)\n • The query matches ALL words provided (in any order). Example: query="flight booking" finds messages with both "flight" AND "booking".\n- Media filters: Use onlyWithMedia for any media, or specific filters like onlyWithVideo, onlyWithImage, onlyWithLink, onlyWithFile for specific types.\n- Pagination: use \'oldestCursor\' + direction=\'before\' for older; \'newestCursor\' + direction=\'after\' for newer.\n- Performance: provide chatIDs/accountIDs when known. Omitted \'query\' returns results based on filters only. Partial matches enabled; \'excludeLowPriority\' defaults to true.\n- Workflow tip: To search messages in specific conversations: 1) Use find-chats to get chatIDs, 2) Use search-messages with those chatIDs.\n- IMPORTANT: Chat names vary widely. ASK the user for clarification:\n • "Which chat do you mean by family?" (could be "The Smiths", "Mom Dad Kids", etc.)\n • "What\'s the name of your work chat?" (could be "Team", company name, project name)\n • "Who are the participants?" (use participantQuery in find-chats)\nReturns: matching messages and referenced chats.', + inputSchema: { + type: 'object', + properties: { + accountIDs: { + type: 'array', + description: 'Limit search to specific Beeper account IDs (bridge instances).', + items: { + type: 'string', + }, + }, + chatIDs: { + type: 'array', + description: 'Limit search to specific Beeper chat IDs.', + items: { + type: 'string', + }, + }, + chatType: { + type: 'string', + description: "Filter by chat type: 'group' for group chats, 'single' for 1:1 chats.", + enum: ['group', 'single'], + }, + dateAfter: { + type: 'string', + description: + "Only include messages with timestamp strictly after this ISO 8601 datetime (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00').", + format: 'date-time', + }, + dateBefore: { + type: 'string', + description: + "Only include messages with timestamp strictly before this ISO 8601 datetime (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00').", + format: 'date-time', + }, + ending_before: { + type: 'string', + description: + 'A cursor for use in pagination. ending_before is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_bar, your subsequent call can include ending_before=obj_bar in order to fetch the previous page of the list.', + }, + excludeLowPriority: { + type: 'boolean', + description: + 'Exclude messages marked Low Priority by the user. Default: true. Set to false to include all.', + }, + includeMuted: { + type: 'boolean', + description: + 'Include messages in chats marked as Muted by the user, which are usually less important. Default: true. Set to false if the user wants a more refined search.', + }, + limit: { + type: 'integer', + description: 'Maximum number of messages to return (1–500). Defaults to 50.', + }, + onlyWithFile: { + type: 'boolean', + description: 'Only return messages that contain file attachments.', + }, + onlyWithImage: { + type: 'boolean', + description: 'Only return messages that contain image attachments.', + }, + onlyWithLink: { + type: 'boolean', + description: 'Only return messages that contain link attachments.', + }, + onlyWithMedia: { + type: 'boolean', + description: 'Only return messages that contain any type of media attachment.', + }, + onlyWithVideo: { + type: 'boolean', + description: 'Only return messages that contain video attachments.', + }, + query: { + type: 'string', + description: + 'Literal word search (NOT semantic). Finds messages containing these EXACT words in any order. Use single words users actually type, not concepts or phrases. Example: use "dinner" not "dinner plans", use "sick" not "health issues". If omitted, returns results filtered only by other parameters.', + }, + sender: { + anyOf: [ + { + type: 'string', + description: + "Filter by sender: 'me' (messages sent by the authenticated user), 'others' (messages sent by others), or a specific user ID string (user.id).", + enum: ['me', 'others'], + }, + { + type: 'string', + }, + ], + description: + "Filter by sender: 'me' (messages sent by the authenticated user), 'others' (messages sent by others), or a specific user ID string (user.id).", + }, + starting_after: { + type: 'string', + description: + 'A cursor for use in pagination. starting_after is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include starting_after=obj_foo in order to fetch the next page of the list.', + }, + }, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + const response = await client.messages.search(body).asResponse(); + return asTextContentResult(await response.json()); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/messages/send-messages.ts b/packages/mcp-server/src/tools/messages/send-messages.ts new file mode 100644 index 0000000..ae823cb --- /dev/null +++ b/packages/mcp-server/src/tools/messages/send-messages.ts @@ -0,0 +1,54 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'messages', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/send-message', + operationId: 'send_message', +}; + +export const tool: Tool = { + name: 'send_messages', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nSend a text message to a specific chat. Supports replying to existing messages. Returns the sent message ID and a deeplink to the chat\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/send_response',\n $defs: {\n send_response: {\n allOf: [ {\n $ref: '#/$defs/base_response'\n }\n ]\n },\n base_response: {\n type: 'object',\n properties: {\n success: {\n type: 'boolean'\n },\n error: {\n type: 'string'\n }\n },\n required: [ 'success'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'The identifier of the chat where the message will send', + }, + replyToMessageID: { + type: 'string', + description: 'Provide a message ID to send this as a reply to an existing message', + }, + text: { + type: 'string', + description: 'Text content of the message you want to send. You may use markdown.', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: ['chatID'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.messages.send(body))); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/oauth/get-user-info-oauth.ts b/packages/mcp-server/src/tools/oauth/get-user-info-oauth.ts new file mode 100644 index 0000000..f925394 --- /dev/null +++ b/packages/mcp-server/src/tools/oauth/get-user-info-oauth.ts @@ -0,0 +1,44 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'oauth', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/oauth/userinfo', + operationId: 'oauth_get_user_info', +}; + +export const tool: Tool = { + name: 'get_user_info_oauth', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nReturns information about the authenticated user/token\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/user_info',\n $defs: {\n user_info: {\n type: 'object',\n properties: {\n iat: {\n type: 'number',\n description: 'Issued at timestamp (Unix epoch seconds)'\n },\n scope: {\n type: 'string',\n description: 'Granted scopes'\n },\n sub: {\n type: 'string',\n description: 'Subject identifier (token ID)'\n },\n token_use: {\n type: 'string',\n description: 'Token type',\n enum: [ 'access'\n ]\n },\n aud: {\n type: 'string',\n description: 'Audience (client ID)'\n },\n client_id: {\n type: 'string',\n description: 'Client identifier'\n },\n exp: {\n type: 'number',\n description: 'Expiration timestamp (Unix epoch seconds)'\n }\n },\n required: [ 'iat',\n 'scope',\n 'sub',\n 'token_use'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.oauth.getUserInfo())); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/oauth/revoke-token-oauth.ts b/packages/mcp-server/src/tools/oauth/revoke-token-oauth.ts new file mode 100644 index 0000000..4262bc6 --- /dev/null +++ b/packages/mcp-server/src/tools/oauth/revoke-token-oauth.ts @@ -0,0 +1,44 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'oauth', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/oauth/revoke', + operationId: 'oauth_revoke_token', +}; + +export const tool: Tool = { + name: 'revoke_token_oauth', + description: 'Revoke an access token or refresh token (RFC 7009)', + inputSchema: { + type: 'object', + properties: { + token: { + type: 'string', + description: 'The token to revoke', + }, + token_type_hint: { + type: 'string', + description: 'Token type hint (RFC 7009)', + enum: ['access_token'], + }, + }, + required: ['token'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + const response = await client.oauth.revokeToken(body).asResponse(); + return asTextContentResult(await response.text()); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/reminders/clear-reminders.ts b/packages/mcp-server/src/tools/reminders/clear-reminders.ts new file mode 100644 index 0000000..8da4cbf --- /dev/null +++ b/packages/mcp-server/src/tools/reminders/clear-reminders.ts @@ -0,0 +1,46 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'reminders', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/clear-chat-reminder', + operationId: 'clear_chat_reminder', +}; + +export const tool: Tool = { + name: 'clear_reminders', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nClear an existing reminder from a chat\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/base_response',\n $defs: {\n base_response: {\n type: 'object',\n properties: {\n success: {\n type: 'boolean'\n },\n error: {\n type: 'string'\n }\n },\n required: [ 'success'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'The identifier of the chat to clear reminder from', + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: ['chatID'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.reminders.clear(body))); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/reminders/set-reminders.ts b/packages/mcp-server/src/tools/reminders/set-reminders.ts new file mode 100644 index 0000000..f206258 --- /dev/null +++ b/packages/mcp-server/src/tools/reminders/set-reminders.ts @@ -0,0 +1,61 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { maybeFilter } from 'beeper/desktop-api-typescript-mcp/filtering'; +import { Metadata, asTextContentResult } from 'beeper/desktop-api-typescript-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from 'desktop-api-typescript'; + +export const metadata: Metadata = { + resource: 'reminders', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/set-chat-reminder', + operationId: 'set_chat_reminder', +}; + +export const tool: Tool = { + name: 'set_reminders', + description: + "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nSet a reminder for a chat at a specific time\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/base_response',\n $defs: {\n base_response: {\n type: 'object',\n properties: {\n success: {\n type: 'boolean'\n },\n error: {\n type: 'string'\n }\n },\n required: [ 'success'\n ]\n }\n }\n}\n```", + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'The identifier of the chat to set reminder for', + }, + reminder: { + type: 'object', + description: 'Reminder configuration', + properties: { + remindAtMs: { + type: 'number', + description: 'Unix timestamp in milliseconds when reminder should trigger', + }, + dismissOnIncomingMessage: { + type: 'boolean', + description: 'Cancel reminder if someone messages in the chat', + }, + }, + required: ['remindAtMs'], + }, + jq_filter: { + type: 'string', + title: 'jq Filter', + description: + 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', + }, + }, + required: ['chatID', 'reminder'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.reminders.set(body))); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/types.ts b/packages/mcp-server/src/tools/types.ts new file mode 100644 index 0000000..df366fc --- /dev/null +++ b/packages/mcp-server/src/tools/types.ts @@ -0,0 +1,103 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from 'desktop-api-typescript'; +import { Tool } from '@modelcontextprotocol/sdk/types.js'; + +type TextContentBlock = { + type: 'text'; + text: string; +}; + +type ImageContentBlock = { + type: 'image'; + data: string; + mimeType: string; +}; + +type AudioContentBlock = { + type: 'audio'; + data: string; + mimeType: string; +}; + +type ResourceContentBlock = { + type: 'resource'; + resource: + | { + uri: string; + mimeType: string; + text: string; + } + | { + uri: string; + mimeType: string; + blob: string; + }; +}; + +export type ContentBlock = TextContentBlock | ImageContentBlock | AudioContentBlock | ResourceContentBlock; + +export type ToolCallResult = { + content: ContentBlock[]; + isError?: boolean; +}; + +export type HandlerFunction = ( + client: BeeperDesktop, + args: Record | undefined, +) => Promise; + +export function asTextContentResult(result: unknown): ToolCallResult { + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} + +export async function asBinaryContentResult(response: Response): Promise { + const blob = await response.blob(); + const mimeType = blob.type; + const data = Buffer.from(await blob.arrayBuffer()).toString('base64'); + if (mimeType.startsWith('image/')) { + return { + content: [{ type: 'image', mimeType, data }], + }; + } else if (mimeType.startsWith('audio/')) { + return { + content: [{ type: 'audio', mimeType, data }], + }; + } else { + return { + content: [ + { + type: 'resource', + resource: { + // We must give a URI, even though this isn't actually an MCP resource. + uri: 'resource://tool-response', + mimeType, + blob: data, + }, + }, + ], + }; + } +} + +export type Metadata = { + resource: string; + operation: 'read' | 'write'; + tags: string[]; + httpMethod?: string; + httpPath?: string; + operationId?: string; +}; + +export type Endpoint = { + metadata: Metadata; + tool: Tool; + handler: HandlerFunction; +}; diff --git a/packages/mcp-server/tests/compat.test.ts b/packages/mcp-server/tests/compat.test.ts new file mode 100644 index 0000000..d6272f6 --- /dev/null +++ b/packages/mcp-server/tests/compat.test.ts @@ -0,0 +1,1166 @@ +import { + truncateToolNames, + removeTopLevelUnions, + removeAnyOf, + inlineRefs, + applyCompatibilityTransformations, + removeFormats, + findUsedDefs, +} from '../src/compat'; +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { JSONSchema } from '../src/compat'; +import { Endpoint } from '../src/tools'; + +describe('truncateToolNames', () => { + it('should return original names when maxLength is 0 or negative', () => { + const names = ['tool1', 'tool2', 'tool3']; + expect(truncateToolNames(names, 0)).toEqual(new Map()); + expect(truncateToolNames(names, -1)).toEqual(new Map()); + }); + + it('should return original names when all names are shorter than maxLength', () => { + const names = ['tool1', 'tool2', 'tool3']; + expect(truncateToolNames(names, 10)).toEqual(new Map()); + }); + + it('should truncate names longer than maxLength', () => { + const names = ['very-long-tool-name', 'another-long-tool-name', 'short']; + expect(truncateToolNames(names, 10)).toEqual( + new Map([ + ['very-long-tool-name', 'very-long-'], + ['another-long-tool-name', 'another-lo'], + ]), + ); + }); + + it('should handle duplicate truncated names by appending numbers', () => { + const names = ['tool-name-a', 'tool-name-b', 'tool-name-c']; + expect(truncateToolNames(names, 8)).toEqual( + new Map([ + ['tool-name-a', 'tool-na1'], + ['tool-name-b', 'tool-na2'], + ['tool-name-c', 'tool-na3'], + ]), + ); + }); +}); + +describe('removeTopLevelUnions', () => { + const createTestTool = (overrides = {}): Tool => ({ + name: 'test-tool', + description: 'Test tool', + inputSchema: { + type: 'object', + properties: {}, + }, + ...overrides, + }); + + it('should return the original tool if it has no anyOf at the top level', () => { + const tool = createTestTool({ + inputSchema: { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + }, + }); + + expect(removeTopLevelUnions(tool)).toEqual([tool]); + }); + + it('should split a tool with top-level anyOf into multiple tools', () => { + const tool = createTestTool({ + name: 'union-tool', + description: 'A tool with unions', + inputSchema: { + type: 'object', + properties: { + common: { type: 'string' }, + }, + anyOf: [ + { + title: 'first variant', + description: 'Its the first variant', + properties: { + variant1: { type: 'string' }, + }, + required: ['variant1'], + }, + { + title: 'second variant', + properties: { + variant2: { type: 'number' }, + }, + required: ['variant2'], + }, + ], + }, + }); + + const result = removeTopLevelUnions(tool); + + expect(result).toEqual([ + { + name: 'union-tool_first_variant', + description: 'Its the first variant', + inputSchema: { + type: 'object', + title: 'first variant', + description: 'Its the first variant', + properties: { + common: { type: 'string' }, + variant1: { type: 'string' }, + }, + required: ['variant1'], + }, + }, + { + name: 'union-tool_second_variant', + description: 'A tool with unions', + inputSchema: { + type: 'object', + title: 'second variant', + description: 'A tool with unions', + properties: { + common: { type: 'string' }, + variant2: { type: 'number' }, + }, + required: ['variant2'], + }, + }, + ]); + }); + + it('should handle $defs and only include those used by the variant', () => { + const tool = createTestTool({ + name: 'defs-tool', + description: 'A tool with $defs', + inputSchema: { + type: 'object', + properties: { + common: { type: 'string' }, + }, + $defs: { + def1: { type: 'string', format: 'email' }, + def2: { type: 'number', minimum: 0 }, + unused: { type: 'boolean' }, + }, + anyOf: [ + { + properties: { + email: { $ref: '#/$defs/def1' }, + }, + }, + { + properties: { + count: { $ref: '#/$defs/def2' }, + }, + }, + ], + }, + }); + + const result = removeTopLevelUnions(tool); + + expect(result).toEqual([ + { + name: 'defs-tool_variant1', + description: 'A tool with $defs', + inputSchema: { + type: 'object', + description: 'A tool with $defs', + properties: { + common: { type: 'string' }, + email: { $ref: '#/$defs/def1' }, + }, + $defs: { + def1: { type: 'string', format: 'email' }, + }, + }, + }, + { + name: 'defs-tool_variant2', + description: 'A tool with $defs', + inputSchema: { + type: 'object', + description: 'A tool with $defs', + properties: { + common: { type: 'string' }, + count: { $ref: '#/$defs/def2' }, + }, + $defs: { + def2: { type: 'number', minimum: 0 }, + }, + }, + }, + ]); + }); +}); + +describe('removeAnyOf', () => { + it('should return original schema if it has no anyOf', () => { + const schema = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' }, + }, + }; + + expect(removeAnyOf(schema)).toEqual(schema); + }); + + it('should remove anyOf field and use the first variant', () => { + const schema = { + type: 'object', + properties: { + common: { type: 'string' }, + }, + anyOf: [ + { + properties: { + variant1: { type: 'string' }, + }, + required: ['variant1'], + }, + { + properties: { + variant2: { type: 'number' }, + }, + required: ['variant2'], + }, + ], + }; + + const expected = { + type: 'object', + properties: { + common: { type: 'string' }, + variant1: { type: 'string' }, + }, + required: ['variant1'], + }; + + expect(removeAnyOf(schema)).toEqual(expected); + }); + + it('should recursively remove anyOf fields from nested properties', () => { + const schema = { + type: 'object', + properties: { + foo: { type: 'string' }, + nested: { + type: 'object', + properties: { + bar: { type: 'number' }, + }, + anyOf: [ + { + properties: { + option1: { type: 'boolean' }, + }, + }, + { + properties: { + option2: { type: 'array' }, + }, + }, + ], + }, + }, + }; + + const expected = { + type: 'object', + properties: { + foo: { type: 'string' }, + nested: { + type: 'object', + properties: { + bar: { type: 'number' }, + option1: { type: 'boolean' }, + }, + }, + }, + }; + + expect(removeAnyOf(schema)).toEqual(expected); + }); + + it('should handle arrays', () => { + const schema = { + type: 'object', + properties: { + items: { + type: 'array', + items: { + anyOf: [{ type: 'string' }, { type: 'number' }], + }, + }, + }, + }; + + const expected = { + type: 'object', + properties: { + items: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + expect(removeAnyOf(schema)).toEqual(expected); + }); +}); + +describe('findUsedDefs', () => { + it('should handle circular references without stack overflow', () => { + const defs = { + person: { + type: 'object', + properties: { + name: { type: 'string' }, + friend: { $ref: '#/$defs/person' }, // Circular reference + }, + }, + }; + + const schema = { + type: 'object', + properties: { + user: { $ref: '#/$defs/person' }, + }, + }; + + // This should not throw a stack overflow error + expect(() => { + const result = findUsedDefs(schema, defs); + expect(result).toHaveProperty('person'); + }).not.toThrow(); + }); + + it('should handle indirect circular references without stack overflow', () => { + const defs = { + node: { + type: 'object', + properties: { + value: { type: 'string' }, + child: { $ref: '#/$defs/childNode' }, + }, + }, + childNode: { + type: 'object', + properties: { + value: { type: 'string' }, + parent: { $ref: '#/$defs/node' }, // Indirect circular reference + }, + }, + }; + + const schema = { + type: 'object', + properties: { + root: { $ref: '#/$defs/node' }, + }, + }; + + // This should not throw a stack overflow error + expect(() => { + const result = findUsedDefs(schema, defs); + expect(result).toHaveProperty('node'); + expect(result).toHaveProperty('childNode'); + }).not.toThrow(); + }); + + it('should find all used definitions in non-circular schemas', () => { + const defs = { + user: { + type: 'object', + properties: { + name: { type: 'string' }, + address: { $ref: '#/$defs/address' }, + }, + }, + address: { + type: 'object', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + }, + }, + unused: { + type: 'object', + properties: { + data: { type: 'string' }, + }, + }, + }; + + const schema = { + type: 'object', + properties: { + person: { $ref: '#/$defs/user' }, + }, + }; + + const result = findUsedDefs(schema, defs); + expect(result).toHaveProperty('user'); + expect(result).toHaveProperty('address'); + expect(result).not.toHaveProperty('unused'); + }); +}); + +describe('inlineRefs', () => { + it('should return the original schema if it does not contain $refs', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + }, + }; + + expect(inlineRefs(schema)).toEqual(schema); + }); + + it('should inline simple $refs', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + user: { $ref: '#/$defs/user' }, + }, + $defs: { + user: { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + }, + }, + }; + + const expected: JSONSchema = { + type: 'object', + properties: { + user: { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + }, + }, + }; + + expect(inlineRefs(schema)).toEqual(expected); + }); + + it('should inline nested $refs', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + order: { $ref: '#/$defs/order' }, + }, + $defs: { + order: { + type: 'object', + properties: { + id: { type: 'string' }, + items: { type: 'array', items: { $ref: '#/$defs/item' } }, + }, + }, + item: { + type: 'object', + properties: { + product: { type: 'string' }, + quantity: { type: 'integer' }, + }, + }, + }, + }; + + const expected: JSONSchema = { + type: 'object', + properties: { + order: { + type: 'object', + properties: { + id: { type: 'string' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + product: { type: 'string' }, + quantity: { type: 'integer' }, + }, + }, + }, + }, + }, + }, + }; + + expect(inlineRefs(schema)).toEqual(expected); + }); + + it('should handle circular references by removing the circular part', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + person: { $ref: '#/$defs/person' }, + }, + $defs: { + person: { + type: 'object', + properties: { + name: { type: 'string' }, + friend: { $ref: '#/$defs/person' }, // Circular reference + }, + }, + }, + }; + + const expected: JSONSchema = { + type: 'object', + properties: { + person: { + type: 'object', + properties: { + name: { type: 'string' }, + // friend property is removed to break the circular reference + }, + }, + }, + }; + + expect(inlineRefs(schema)).toEqual(expected); + }); + + it('should handle indirect circular references', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + node: { $ref: '#/$defs/node' }, + }, + $defs: { + node: { + type: 'object', + properties: { + value: { type: 'string' }, + child: { $ref: '#/$defs/childNode' }, + }, + }, + childNode: { + type: 'object', + properties: { + value: { type: 'string' }, + parent: { $ref: '#/$defs/node' }, // Circular reference through childNode + }, + }, + }, + }; + + const expected: JSONSchema = { + type: 'object', + properties: { + node: { + type: 'object', + properties: { + value: { type: 'string' }, + child: { + type: 'object', + properties: { + value: { type: 'string' }, + // parent property is removed to break the circular reference + }, + }, + }, + }, + }, + }; + + expect(inlineRefs(schema)).toEqual(expected); + }); + + it('should preserve other properties when inlining references', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + address: { $ref: '#/$defs/address', description: 'User address' }, + }, + $defs: { + address: { + type: 'object', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + }, + required: ['street'], + }, + }, + }; + + const expected: JSONSchema = { + type: 'object', + properties: { + address: { + type: 'object', + description: 'User address', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + }, + required: ['street'], + }, + }, + }; + + expect(inlineRefs(schema)).toEqual(expected); + }); +}); + +describe('removeFormats', () => { + it('should return original schema if formats capability is true', () => { + const schema = { + type: 'object', + properties: { + date: { type: 'string', description: 'A date field', format: 'date' }, + email: { type: 'string', description: 'An email field', format: 'email' }, + }, + }; + + expect(removeFormats(schema, true)).toEqual(schema); + }); + + it('should move format to description when formats capability is false', () => { + const schema = { + type: 'object', + properties: { + date: { type: 'string', description: 'A date field', format: 'date' }, + email: { type: 'string', description: 'An email field', format: 'email' }, + }, + }; + + const expected = { + type: 'object', + properties: { + date: { type: 'string', description: 'A date field (format: "date")' }, + email: { type: 'string', description: 'An email field (format: "email")' }, + }, + }; + + expect(removeFormats(schema, false)).toEqual(expected); + }); + + it('should handle properties without description', () => { + const schema = { + type: 'object', + properties: { + date: { type: 'string', format: 'date' }, + }, + }; + + const expected = { + type: 'object', + properties: { + date: { type: 'string', description: '(format: "date")' }, + }, + }; + + expect(removeFormats(schema, false)).toEqual(expected); + }); + + it('should handle nested properties', () => { + const schema = { + type: 'object', + properties: { + user: { + type: 'object', + properties: { + created_at: { type: 'string', description: 'Creation date', format: 'date-time' }, + }, + }, + }, + }; + + const expected = { + type: 'object', + properties: { + user: { + type: 'object', + properties: { + created_at: { type: 'string', description: 'Creation date (format: "date-time")' }, + }, + }, + }, + }; + + expect(removeFormats(schema, false)).toEqual(expected); + }); + + it('should handle arrays of objects', () => { + const schema = { + type: 'object', + properties: { + dates: { + type: 'array', + items: { + type: 'object', + properties: { + start: { type: 'string', description: 'Start date', format: 'date' }, + end: { type: 'string', description: 'End date', format: 'date' }, + }, + }, + }, + }, + }; + + const expected = { + type: 'object', + properties: { + dates: { + type: 'array', + items: { + type: 'object', + properties: { + start: { type: 'string', description: 'Start date (format: "date")' }, + end: { type: 'string', description: 'End date (format: "date")' }, + }, + }, + }, + }, + }; + + expect(removeFormats(schema, false)).toEqual(expected); + }); + + it('should handle schemas with $defs', () => { + const schema = { + type: 'object', + properties: { + date: { type: 'string', description: 'A date field', format: 'date' }, + }, + $defs: { + timestamp: { + type: 'string', + description: 'A timestamp field', + format: 'date-time', + }, + }, + }; + + const expected = { + type: 'object', + properties: { + date: { type: 'string', description: 'A date field (format: "date")' }, + }, + $defs: { + timestamp: { + type: 'string', + description: 'A timestamp field (format: "date-time")', + }, + }, + }; + + expect(removeFormats(schema, false)).toEqual(expected); + }); +}); + +describe('applyCompatibilityTransformations', () => { + const createTestTool = (name: string, overrides = {}): Tool => ({ + name, + description: 'Test tool', + inputSchema: { + type: 'object', + properties: {}, + }, + ...overrides, + }); + + const createTestEndpoint = (tool: Tool): Endpoint => ({ + tool, + handler: jest.fn(), + metadata: { + resource: 'test', + operation: 'read' as const, + tags: [], + }, + }); + + it('should not modify endpoints when all capabilities are enabled', () => { + const tool = createTestTool('test-tool'); + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + expect(transformed).toEqual(endpoints); + }); + + it('should split tools with top-level unions when topLevelUnions is disabled', () => { + const tool = createTestTool('union-tool', { + inputSchema: { + type: 'object', + properties: { + common: { type: 'string' }, + }, + anyOf: [ + { + title: 'first variant', + properties: { + variant1: { type: 'string' }, + }, + }, + { + title: 'second variant', + properties: { + variant2: { type: 'number' }, + }, + }, + ], + }, + }); + + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: false, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + expect(transformed.length).toBe(2); + expect(transformed[0]!.tool.name).toBe('union-tool_first_variant'); + expect(transformed[1]!.tool.name).toBe('union-tool_second_variant'); + }); + + it('should handle variants without titles in removeTopLevelUnions', () => { + const tool = createTestTool('union-tool', { + inputSchema: { + type: 'object', + properties: { + common: { type: 'string' }, + }, + anyOf: [ + { + properties: { + variant1: { type: 'string' }, + }, + }, + { + properties: { + variant2: { type: 'number' }, + }, + }, + ], + }, + }); + + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: false, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + expect(transformed.length).toBe(2); + expect(transformed[0]!.tool.name).toBe('union-tool_variant1'); + expect(transformed[1]!.tool.name).toBe('union-tool_variant2'); + }); + + it('should truncate tool names when toolNameLength is set', () => { + const tools = [ + createTestTool('very-long-tool-name-that-exceeds-limit'), + createTestTool('another-long-tool-name-to-truncate'), + createTestTool('short-name'), + ]; + + const endpoints = tools.map(createTestEndpoint); + + const capabilities = { + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: 20, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + expect(transformed[0]!.tool.name).toBe('very-long-tool-name-'); + expect(transformed[1]!.tool.name).toBe('another-long-tool-na'); + expect(transformed[2]!.tool.name).toBe('short-name'); + }); + + it('should inline refs when refs capability is disabled', () => { + const tool = createTestTool('ref-tool', { + inputSchema: { + type: 'object', + properties: { + user: { $ref: '#/$defs/user' }, + }, + $defs: { + user: { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + }, + }, + }, + }); + + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: true, + validJson: true, + refs: false, + unions: true, + formats: true, + toolNameLength: undefined, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + const schema = transformed[0]!.tool.inputSchema as JSONSchema; + expect(schema.$defs).toBeUndefined(); + + if (schema.properties) { + expect(schema.properties['user']).toEqual({ + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + }); + } + }); + + it('should preserve external refs when inlining', () => { + const tool = createTestTool('ref-tool', { + inputSchema: { + type: 'object', + properties: { + internal: { $ref: '#/$defs/internal' }, + external: { $ref: 'https://example.com/schemas/external.json' }, + }, + $defs: { + internal: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }, + }, + }, + }); + + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: true, + validJson: true, + refs: false, + unions: true, + formats: true, + toolNameLength: undefined, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + const schema = transformed[0]!.tool.inputSchema as JSONSchema; + + if (schema.properties) { + expect(schema.properties['internal']).toEqual({ + type: 'object', + properties: { + name: { type: 'string' }, + }, + }); + expect(schema.properties['external']).toEqual({ + $ref: 'https://example.com/schemas/external.json', + }); + } + }); + + it('should remove anyOf fields when unions capability is disabled', () => { + const tool = createTestTool('union-tool', { + inputSchema: { + type: 'object', + properties: { + field: { + anyOf: [{ type: 'string' }, { type: 'number' }], + }, + }, + }, + }); + + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: true, + validJson: true, + refs: true, + unions: false, + formats: true, + toolNameLength: undefined, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + const schema = transformed[0]!.tool.inputSchema as JSONSchema; + + if (schema.properties && schema.properties['field']) { + const field = schema.properties['field']; + expect(field.anyOf).toBeUndefined(); + expect(field.type).toBe('string'); + } + }); + + it('should correctly combine topLevelUnions and toolNameLength transformations', () => { + const tool = createTestTool('very-long-union-tool-name', { + inputSchema: { + type: 'object', + properties: { + common: { type: 'string' }, + }, + anyOf: [ + { + title: 'first variant', + properties: { + variant1: { type: 'string' }, + }, + }, + { + title: 'second variant', + properties: { + variant2: { type: 'number' }, + }, + }, + ], + }, + }); + + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: false, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: 20, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + expect(transformed.length).toBe(2); + + // Both names should be truncated because they exceed 20 characters + expect(transformed[0]!.tool.name).toBe('very-long-union-too1'); + expect(transformed[1]!.tool.name).toBe('very-long-union-too2'); + }); + + it('should correctly combine refs and unions transformations', () => { + const tool = createTestTool('complex-tool', { + inputSchema: { + type: 'object', + properties: { + user: { $ref: '#/$defs/user' }, + }, + $defs: { + user: { + type: 'object', + properties: { + preference: { + anyOf: [{ type: 'string' }, { type: 'number' }], + }, + }, + }, + }, + }, + }); + + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: true, + validJson: true, + refs: false, + unions: false, + formats: true, + toolNameLength: undefined, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + const schema = transformed[0]!.tool.inputSchema as JSONSchema; + + // Refs should be inlined + expect(schema.$defs).toBeUndefined(); + + // Safely access nested properties + if (schema.properties && schema.properties['user']) { + const user = schema.properties['user']; + // User should be inlined + expect(user.type).toBe('object'); + + // AnyOf in the inlined user.preference should be removed + if (user.properties && user.properties['preference']) { + const preference = user.properties['preference']; + expect(preference.anyOf).toBeUndefined(); + expect(preference.type).toBe('string'); + } + } + }); + + it('should handle formats capability being false', () => { + const tool = createTestTool('format-tool', { + inputSchema: { + type: 'object', + properties: { + date: { type: 'string', description: 'A date', format: 'date' }, + }, + }, + }); + + const endpoints = [createTestEndpoint(tool)]; + + const capabilities = { + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: false, + toolNameLength: undefined, + }; + + const transformed = applyCompatibilityTransformations(endpoints, capabilities); + const schema = transformed[0]!.tool.inputSchema as JSONSchema; + + if (schema.properties && schema.properties['date']) { + const dateField = schema.properties['date']; + expect(dateField['format']).toBeUndefined(); + expect(dateField['description']).toBe('A date (format: "date")'); + } + }); +}); diff --git a/packages/mcp-server/tests/dynamic-tools.test.ts b/packages/mcp-server/tests/dynamic-tools.test.ts new file mode 100644 index 0000000..08963af --- /dev/null +++ b/packages/mcp-server/tests/dynamic-tools.test.ts @@ -0,0 +1,185 @@ +import { dynamicTools } from '../src/dynamic-tools'; +import { Endpoint } from '../src/tools'; + +describe('dynamicTools', () => { + const fakeClient = {} as any; + + const endpoints: Endpoint[] = [ + makeEndpoint('test_read_endpoint', 'test_resource', 'read', ['test']), + makeEndpoint('test_write_endpoint', 'test_resource', 'write', ['test']), + makeEndpoint('user_endpoint', 'user', 'read', ['user', 'admin']), + makeEndpoint('admin_endpoint', 'admin', 'write', ['admin']), + ]; + + const tools = dynamicTools(endpoints); + + const toolsMap = { + list_api_endpoints: toolOrError('list_api_endpoints'), + get_api_endpoint_schema: toolOrError('get_api_endpoint_schema'), + invoke_api_endpoint: toolOrError('invoke_api_endpoint'), + }; + + describe('list_api_endpoints', () => { + it('should return all endpoints when no search query is provided', async () => { + const content = await toolsMap.list_api_endpoints.handler(fakeClient, {}); + const result = JSON.parse(content.content[0].text); + + expect(result.tools).toHaveLength(endpoints.length); + expect(result.tools.map((t: { name: string }) => t.name)).toContain('test_read_endpoint'); + expect(result.tools.map((t: { name: string }) => t.name)).toContain('test_write_endpoint'); + expect(result.tools.map((t: { name: string }) => t.name)).toContain('user_endpoint'); + expect(result.tools.map((t: { name: string }) => t.name)).toContain('admin_endpoint'); + }); + + it('should filter endpoints by name', async () => { + const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'user' }); + const result = JSON.parse(content.content[0].text); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('user_endpoint'); + }); + + it('should filter endpoints by resource', async () => { + const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'admin' }); + const result = JSON.parse(content.content[0].text); + + expect(result.tools.some((t: { resource: string }) => t.resource === 'admin')).toBeTruthy(); + }); + + it('should filter endpoints by tag', async () => { + const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'admin' }); + const result = JSON.parse(content.content[0].text); + + expect(result.tools.some((t: { tags: string[] }) => t.tags.includes('admin'))).toBeTruthy(); + }); + + it('should be case insensitive in search', async () => { + const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'ADMIN' }); + const result = JSON.parse(content.content[0].text); + + expect(result.tools.length).toBe(2); + result.tools.forEach((tool: { name: string; resource: string; tags: string[] }) => { + expect( + tool.name.toLowerCase().includes('admin') || + tool.resource.toLowerCase().includes('admin') || + tool.tags.some((tag: string) => tag.toLowerCase().includes('admin')), + ).toBeTruthy(); + }); + }); + + it('should filter endpoints by description', async () => { + const content = await toolsMap.list_api_endpoints.handler(fakeClient, { + search_query: 'Test endpoint for user_endpoint', + }); + const result = JSON.parse(content.content[0].text); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('user_endpoint'); + expect(result.tools[0].description).toBe('Test endpoint for user_endpoint'); + }); + + it('should filter endpoints by partial description match', async () => { + const content = await toolsMap.list_api_endpoints.handler(fakeClient, { + search_query: 'endpoint for user', + }); + const result = JSON.parse(content.content[0].text); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe('user_endpoint'); + }); + }); + + describe('get_api_endpoint_schema', () => { + it('should return schema for existing endpoint', async () => { + const content = await toolsMap.get_api_endpoint_schema.handler(fakeClient, { + endpoint: 'test_read_endpoint', + }); + const result = JSON.parse(content.content[0].text); + + expect(result).toEqual(endpoints[0]?.tool); + }); + + it('should throw error for non-existent endpoint', async () => { + await expect( + toolsMap.get_api_endpoint_schema.handler(fakeClient, { endpoint: 'non_existent_endpoint' }), + ).rejects.toThrow('Endpoint non_existent_endpoint not found'); + }); + + it('should throw error when no endpoint provided', async () => { + await expect(toolsMap.get_api_endpoint_schema.handler(fakeClient, undefined)).rejects.toThrow( + 'No endpoint provided', + ); + }); + }); + + describe('invoke_api_endpoint', () => { + it('should successfully invoke endpoint with valid arguments', async () => { + const mockHandler = endpoints[0]?.handler as jest.Mock; + mockHandler.mockClear(); + + await toolsMap.invoke_api_endpoint.handler(fakeClient, { + endpoint_name: 'test_read_endpoint', + args: { testParam: 'test value' }, + }); + + expect(mockHandler).toHaveBeenCalledWith(fakeClient, { testParam: 'test value' }); + }); + + it('should throw error for non-existent endpoint', async () => { + await expect( + toolsMap.invoke_api_endpoint.handler(fakeClient, { + endpoint_name: 'non_existent_endpoint', + args: { testParam: 'test value' }, + }), + ).rejects.toThrow(/Endpoint non_existent_endpoint not found/); + }); + + it('should throw error when no arguments provided', async () => { + await expect(toolsMap.invoke_api_endpoint.handler(fakeClient, undefined)).rejects.toThrow( + 'No endpoint provided', + ); + }); + + it('should throw error for invalid argument schema', async () => { + await expect( + toolsMap.invoke_api_endpoint.handler(fakeClient, { + endpoint_name: 'test_read_endpoint', + args: { wrongParam: 'test value' }, // Missing required testParam + }), + ).rejects.toThrow(/Invalid arguments for endpoint/); + }); + }); + + function toolOrError(name: string) { + const tool = tools.find((tool) => tool.tool.name === name); + if (!tool) throw new Error(`Tool ${name} not found`); + return tool; + } +}); + +function makeEndpoint( + name: string, + resource: string, + operation: 'read' | 'write', + tags: string[] = [], +): Endpoint { + return { + metadata: { + resource, + operation, + tags, + }, + tool: { + name, + description: `Test endpoint for ${name}`, + inputSchema: { + type: 'object', + properties: { + testParam: { type: 'string' }, + }, + required: ['testParam'], + }, + }, + handler: jest.fn().mockResolvedValue({ success: true }), + }; +} diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts new file mode 100644 index 0000000..a8a5b81 --- /dev/null +++ b/packages/mcp-server/tests/options.test.ts @@ -0,0 +1,518 @@ +import { parseCLIOptions, parseQueryOptions } from '../src/options'; +import { Filter } from '../src/tools'; +import { parseEmbeddedJSON } from '../src/compat'; + +// Mock process.argv +const mockArgv = (args: string[]) => { + const originalArgv = process.argv; + process.argv = ['node', 'test.js', ...args]; + return () => { + process.argv = originalArgv; + }; +}; + +describe('parseCLIOptions', () => { + it('should parse basic filter options', () => { + const cleanup = mockArgv([ + '--tool=test-tool', + '--resource=test-resource', + '--operation=read', + '--tag=test-tag', + ]); + + const result = parseCLIOptions(); + + expect(result.filters).toEqual([ + { type: 'tag', op: 'include', value: 'test-tag' }, + { type: 'resource', op: 'include', value: 'test-resource' }, + { type: 'tool', op: 'include', value: 'test-tool' }, + { type: 'operation', op: 'include', value: 'read' }, + ] as Filter[]); + + expect(result.capabilities).toEqual({}); + + expect(result.list).toBe(false); + + cleanup(); + }); + + it('should parse exclusion filters', () => { + const cleanup = mockArgv([ + '--no-tool=exclude-tool', + '--no-resource=exclude-resource', + '--no-operation=write', + '--no-tag=exclude-tag', + ]); + + const result = parseCLIOptions(); + + expect(result.filters).toEqual([ + { type: 'tag', op: 'exclude', value: 'exclude-tag' }, + { type: 'resource', op: 'exclude', value: 'exclude-resource' }, + { type: 'tool', op: 'exclude', value: 'exclude-tool' }, + { type: 'operation', op: 'exclude', value: 'write' }, + ] as Filter[]); + + expect(result.capabilities).toEqual({}); + + cleanup(); + }); + + it('should parse client presets', () => { + const cleanup = mockArgv(['--client=openai-agents']); + + const result = parseCLIOptions(); + + expect(result.client).toEqual('openai-agents'); + + cleanup(); + }); + + it('should parse individual capabilities', () => { + const cleanup = mockArgv([ + '--capability=top-level-unions', + '--capability=valid-json', + '--capability=refs', + '--capability=unions', + '--capability=tool-name-length=40', + ]); + + const result = parseCLIOptions(); + + expect(result.capabilities).toEqual({ + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + toolNameLength: 40, + }); + + cleanup(); + }); + + it('should handle list option', () => { + const cleanup = mockArgv(['--list']); + + const result = parseCLIOptions(); + + expect(result.list).toBe(true); + + cleanup(); + }); + + it('should handle multiple filters of the same type', () => { + const cleanup = mockArgv(['--tool=tool1', '--tool=tool2', '--resource=res1', '--resource=res2']); + + const result = parseCLIOptions(); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'include', value: 'res1' }, + { type: 'resource', op: 'include', value: 'res2' }, + { type: 'tool', op: 'include', value: 'tool1' }, + { type: 'tool', op: 'include', value: 'tool2' }, + ] as Filter[]); + + cleanup(); + }); + + it('should handle comma-separated values in array options', () => { + const cleanup = mockArgv([ + '--tool=tool1,tool2', + '--resource=res1,res2', + '--capability=top-level-unions,valid-json,unions', + ]); + + const result = parseCLIOptions(); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'include', value: 'res1' }, + { type: 'resource', op: 'include', value: 'res2' }, + { type: 'tool', op: 'include', value: 'tool1' }, + { type: 'tool', op: 'include', value: 'tool2' }, + ] as Filter[]); + + expect(result.capabilities).toEqual({ + topLevelUnions: true, + validJson: true, + unions: true, + }); + + cleanup(); + }); + + it('should handle invalid tool-name-length format', () => { + const cleanup = mockArgv(['--capability=tool-name-length=invalid']); + + // Mock console.error to prevent output during test + const originalError = console.error; + console.error = jest.fn(); + + expect(() => parseCLIOptions()).toThrow(); + + console.error = originalError; + cleanup(); + }); + + it('should handle unknown capability', () => { + const cleanup = mockArgv(['--capability=unknown-capability']); + + // Mock console.error to prevent output during test + const originalError = console.error; + console.error = jest.fn(); + + expect(() => parseCLIOptions()).toThrow(); + + console.error = originalError; + cleanup(); + }); +}); + +describe('parseQueryOptions', () => { + const defaultOptions = { + client: undefined, + includeDynamicTools: undefined, + includeAllTools: undefined, + filters: [], + capabilities: { + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }, + }; + + it('should parse basic filter options from query string', () => { + const query = 'tool=test-tool&resource=test-resource&operation=read&tag=test-tag'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'include', value: 'test-resource' }, + { type: 'operation', op: 'include', value: 'read' }, + { type: 'tag', op: 'include', value: 'test-tag' }, + { type: 'tool', op: 'include', value: 'test-tool' }, + ]); + + expect(result.capabilities).toEqual({ + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: undefined, + }); + }); + + it('should parse exclusion filters from query string', () => { + const query = 'no_tool=exclude-tool&no_resource=exclude-resource&no_operation=write&no_tag=exclude-tag'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'exclude', value: 'exclude-resource' }, + { type: 'operation', op: 'exclude', value: 'write' }, + { type: 'tag', op: 'exclude', value: 'exclude-tag' }, + { type: 'tool', op: 'exclude', value: 'exclude-tool' }, + ]); + }); + + it('should parse client option from query string', () => { + const query = 'client=openai-agents'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.client).toBe('openai-agents'); + }); + + it('should parse client capabilities from query string', () => { + const query = 'capability=top-level-unions&capability=valid-json&capability=tool-name-length%3D40'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.capabilities).toEqual({ + topLevelUnions: true, + validJson: true, + refs: true, + unions: true, + formats: true, + toolNameLength: 40, + }); + }); + + it('should parse no-capability options from query string', () => { + const query = 'no_capability=top-level-unions&no_capability=refs&no_capability=formats'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.capabilities).toEqual({ + topLevelUnions: false, + validJson: true, + refs: false, + unions: true, + formats: false, + toolNameLength: undefined, + }); + }); + + it('should parse tools options from query string', () => { + const query = 'tools=dynamic&tools=all'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.includeDynamicTools).toBe(true); + expect(result.includeAllTools).toBe(true); + }); + + it('should parse no-tools options from query string', () => { + const query = 'tools=dynamic&tools=all&no_tools=dynamic'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.includeDynamicTools).toBe(false); + expect(result.includeAllTools).toBe(true); + }); + + it('should handle array values in query string', () => { + const query = 'tool[]=tool1&tool[]=tool2&resource[]=res1&resource[]=res2'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'include', value: 'res1' }, + { type: 'resource', op: 'include', value: 'res2' }, + { type: 'tool', op: 'include', value: 'tool1' }, + { type: 'tool', op: 'include', value: 'tool2' }, + ]); + }); + + it('should merge with default options', () => { + const defaultWithFilters = { + ...defaultOptions, + filters: [{ type: 'tag' as const, op: 'include' as const, value: 'existing-tag' }], + client: 'cursor' as const, + includeDynamicTools: true, + }; + + const query = 'tool=new-tool&resource=new-resource'; + const result = parseQueryOptions(defaultWithFilters, query); + + expect(result.filters).toEqual([ + { type: 'tag', op: 'include', value: 'existing-tag' }, + { type: 'resource', op: 'include', value: 'new-resource' }, + { type: 'tool', op: 'include', value: 'new-tool' }, + ]); + + expect(result.client).toBe('cursor'); + expect(result.includeDynamicTools).toBe(true); + }); + + it('should override client from default options', () => { + const defaultWithClient = { + ...defaultOptions, + client: 'cursor' as const, + }; + + const query = 'client=openai-agents'; + const result = parseQueryOptions(defaultWithClient, query); + + expect(result.client).toBe('openai-agents'); + }); + + it('should merge capabilities with default options', () => { + const defaultWithCapabilities = { + ...defaultOptions, + capabilities: { + topLevelUnions: false, + validJson: false, + refs: true, + unions: true, + formats: true, + toolNameLength: 30, + }, + }; + + const query = 'capability=top-level-unions&no_capability=refs'; + const result = parseQueryOptions(defaultWithCapabilities, query); + + expect(result.capabilities).toEqual({ + topLevelUnions: true, + validJson: false, + refs: false, + unions: true, + formats: true, + toolNameLength: 30, + }); + }); + + it('should handle empty query string', () => { + const query = ''; + const result = parseQueryOptions(defaultOptions, query); + + expect(result).toEqual(defaultOptions); + }); + + it('should handle invalid query string gracefully', () => { + const query = 'invalid=value&operation=invalid-operation'; + + // Should throw due to Zod validation for invalid operation + expect(() => parseQueryOptions(defaultOptions, query)).toThrow(); + }); + + it('should preserve default undefined values when not specified', () => { + const defaultWithUndefined = { + ...defaultOptions, + client: undefined, + includeDynamicTools: undefined, + includeAllTools: undefined, + }; + + const query = 'tool=test-tool'; + const result = parseQueryOptions(defaultWithUndefined, query); + + expect(result.client).toBeUndefined(); + expect(result.includeDynamicTools).toBeFalsy(); + expect(result.includeAllTools).toBeFalsy(); + }); + + it('should handle complex query with mixed include and exclude filters', () => { + const query = + 'tool=include-tool&no_tool=exclude-tool&resource=include-res&no_resource=exclude-res&operation=read&tag=include-tag&no_tag=exclude-tag'; + const result = parseQueryOptions(defaultOptions, query); + + expect(result.filters).toEqual([ + { type: 'resource', op: 'include', value: 'include-res' }, + { type: 'operation', op: 'include', value: 'read' }, + { type: 'tag', op: 'include', value: 'include-tag' }, + { type: 'tool', op: 'include', value: 'include-tool' }, + { type: 'resource', op: 'exclude', value: 'exclude-res' }, + { type: 'tag', op: 'exclude', value: 'exclude-tag' }, + { type: 'tool', op: 'exclude', value: 'exclude-tool' }, + ]); + }); +}); + +describe('parseEmbeddedJSON', () => { + it('should not change non-string values', () => { + const args = { + numberProp: 42, + booleanProp: true, + objectProp: { nested: 'value' }, + arrayProp: [1, 2, 3], + nullProp: null, + undefinedProp: undefined, + }; + const schema = {}; + + const result = parseEmbeddedJSON(args, schema); + + expect(result).toBe(args); // Should return original object since no changes made + expect(result['numberProp']).toBe(42); + expect(result['booleanProp']).toBe(true); + expect(result['objectProp']).toEqual({ nested: 'value' }); + expect(result['arrayProp']).toEqual([1, 2, 3]); + expect(result['nullProp']).toBe(null); + expect(result['undefinedProp']).toBe(undefined); + }); + + it('should parse valid JSON objects in string properties', () => { + const args = { + jsonObjectString: '{"key": "value", "number": 123}', + regularString: 'not json', + }; + const schema = {}; + + const result = parseEmbeddedJSON(args, schema); + + expect(result).not.toBe(args); // Should return new object since changes were made + expect(result['jsonObjectString']).toEqual({ key: 'value', number: 123 }); + expect(result['regularString']).toBe('not json'); + }); + + it('should leave invalid JSON in string properties unchanged', () => { + const args = { + invalidJson1: '{"key": value}', // Missing quotes around value + invalidJson2: '{key: "value"}', // Missing quotes around key + invalidJson3: '{"key": "value",}', // Trailing comma + invalidJson4: 'just a regular string', + emptyString: '', + }; + const schema = {}; + + const result = parseEmbeddedJSON(args, schema); + + expect(result).toBe(args); // Should return original object since no changes made + expect(result['invalidJson1']).toBe('{"key": value}'); + expect(result['invalidJson2']).toBe('{key: "value"}'); + expect(result['invalidJson3']).toBe('{"key": "value",}'); + expect(result['invalidJson4']).toBe('just a regular string'); + expect(result['emptyString']).toBe(''); + }); + + it('should not parse JSON primitives in string properties', () => { + const args = { + numberString: '123', + floatString: '45.67', + negativeNumberString: '-89', + booleanTrueString: 'true', + booleanFalseString: 'false', + nullString: 'null', + jsonArrayString: '[1, 2, 3, "test"]', + regularString: 'not json', + }; + const schema = {}; + + const result = parseEmbeddedJSON(args, schema); + + expect(result).toBe(args); // Should return original object since no changes made + expect(result['numberString']).toBe('123'); + expect(result['floatString']).toBe('45.67'); + expect(result['negativeNumberString']).toBe('-89'); + expect(result['booleanTrueString']).toBe('true'); + expect(result['booleanFalseString']).toBe('false'); + expect(result['nullString']).toBe('null'); + expect(result['jsonArrayString']).toBe('[1, 2, 3, "test"]'); + expect(result['regularString']).toBe('not json'); + }); + + it('should handle mixed valid objects and other JSON types', () => { + const args = { + validObject: '{"success": true}', + invalidObject: '{"missing": quote}', + validNumber: '42', + validArray: '[1, 2, 3]', + keepAsString: 'hello world', + nonString: 123, + }; + const schema = {}; + + const result = parseEmbeddedJSON(args, schema); + + expect(result).not.toBe(args); // Should return new object since some changes were made + expect(result['validObject']).toEqual({ success: true }); + expect(result['invalidObject']).toBe('{"missing": quote}'); + expect(result['validNumber']).toBe('42'); // Not parsed, remains string + expect(result['validArray']).toBe('[1, 2, 3]'); // Not parsed, remains string + expect(result['keepAsString']).toBe('hello world'); + expect(result['nonString']).toBe(123); + }); + + it('should return original object when no strings are present', () => { + const args = { + number: 42, + boolean: true, + object: { key: 'value' }, + }; + const schema = {}; + + const result = parseEmbeddedJSON(args, schema); + + expect(result).toBe(args); // Should return original object since no changes made + }); + + it('should return original object when all strings are invalid JSON', () => { + const args = { + string1: 'hello', + string2: 'world', + string3: 'not json at all', + }; + const schema = {}; + + const result = parseEmbeddedJSON(args, schema); + + expect(result).toBe(args); // Should return original object since no changes made + }); +}); diff --git a/packages/mcp-server/tests/tools.test.ts b/packages/mcp-server/tests/tools.test.ts new file mode 100644 index 0000000..cfff24a --- /dev/null +++ b/packages/mcp-server/tests/tools.test.ts @@ -0,0 +1,225 @@ +import { Endpoint, Filter, Metadata, query } from '../src/tools'; + +describe('Endpoint filtering', () => { + const endpoints: Endpoint[] = [ + endpoint({ + resource: 'user', + operation: 'read', + tags: ['admin'], + toolName: 'retrieve_user', + }), + endpoint({ + resource: 'user.profile', + operation: 'write', + tags: [], + toolName: 'create_user_profile', + }), + endpoint({ + resource: 'user.profile', + operation: 'read', + tags: [], + toolName: 'get_user_profile', + }), + endpoint({ + resource: 'user.roles.permissions', + operation: 'write', + tags: ['admin', 'security'], + toolName: 'update_user_role_permissions', + }), + endpoint({ + resource: 'documents.metadata.tags', + operation: 'write', + tags: ['taxonomy', 'metadata'], + toolName: 'create_document_metadata_tags', + }), + endpoint({ + resource: 'organization.settings', + operation: 'read', + tags: ['admin', 'configuration'], + toolName: 'get_organization_settings', + }), + ]; + + const tests: { name: string; filters: Filter[]; expected: string[] }[] = [ + { + name: 'match none', + filters: [], + expected: [], + }, + + // Resource tests + { + name: 'simple resource', + filters: [{ type: 'resource', op: 'include', value: 'user' }], + expected: ['retrieve_user'], + }, + { + name: 'exclude resource', + filters: [{ type: 'resource', op: 'exclude', value: 'user' }], + expected: [ + 'create_user_profile', + 'get_user_profile', + 'update_user_role_permissions', + 'create_document_metadata_tags', + 'get_organization_settings', + ], + }, + { + name: 'resource and subresources', + filters: [{ type: 'resource', op: 'include', value: 'user*' }], + expected: ['retrieve_user', 'create_user_profile', 'get_user_profile', 'update_user_role_permissions'], + }, + { + name: 'just subresources', + filters: [{ type: 'resource', op: 'include', value: 'user.*' }], + expected: ['create_user_profile', 'get_user_profile', 'update_user_role_permissions'], + }, + { + name: 'specific subresource', + filters: [{ type: 'resource', op: 'include', value: 'user.roles.permissions' }], + expected: ['update_user_role_permissions'], + }, + { + name: 'deep wildcard match', + filters: [{ type: 'resource', op: 'include', value: '*.*.tags' }], + expected: ['create_document_metadata_tags'], + }, + + // Operation tests + { + name: 'read operation', + filters: [{ type: 'operation', op: 'include', value: 'read' }], + expected: ['retrieve_user', 'get_user_profile', 'get_organization_settings'], + }, + { + name: 'write operation', + filters: [{ type: 'operation', op: 'include', value: 'write' }], + expected: ['create_user_profile', 'update_user_role_permissions', 'create_document_metadata_tags'], + }, + { + name: 'resource and operation combined', + filters: [ + { type: 'resource', op: 'include', value: 'user.profile' }, + { type: 'operation', op: 'exclude', value: 'write' }, + ], + expected: ['get_user_profile'], + }, + + // Tag tests + { + name: 'admin tag', + filters: [{ type: 'tag', op: 'include', value: 'admin' }], + expected: ['retrieve_user', 'update_user_role_permissions', 'get_organization_settings'], + }, + { + name: 'taxonomy tag', + filters: [{ type: 'tag', op: 'include', value: 'taxonomy' }], + expected: ['create_document_metadata_tags'], + }, + { + name: 'multiple tags (OR logic)', + filters: [ + { type: 'tag', op: 'include', value: 'admin' }, + { type: 'tag', op: 'include', value: 'security' }, + ], + expected: ['retrieve_user', 'update_user_role_permissions', 'get_organization_settings'], + }, + { + name: 'excluding a tag', + filters: [ + { type: 'tag', op: 'include', value: 'admin' }, + { type: 'tag', op: 'exclude', value: 'security' }, + ], + expected: ['retrieve_user', 'get_organization_settings'], + }, + + // Tool name tests + { + name: 'tool name match', + filters: [{ type: 'tool', op: 'include', value: 'get_organization_settings' }], + expected: ['get_organization_settings'], + }, + { + name: 'two tools match', + filters: [ + { type: 'tool', op: 'include', value: 'get_organization_settings' }, + { type: 'tool', op: 'include', value: 'create_user_profile' }, + ], + expected: ['create_user_profile', 'get_organization_settings'], + }, + { + name: 'excluding tool by name', + filters: [ + { type: 'resource', op: 'include', value: 'user*' }, + { type: 'tool', op: 'exclude', value: 'retrieve_user' }, + ], + expected: ['create_user_profile', 'get_user_profile', 'update_user_role_permissions'], + }, + + // Complex combinations + { + name: 'complex filter: read operations with admin tag', + filters: [ + { type: 'operation', op: 'include', value: 'read' }, + { type: 'tag', op: 'include', value: 'admin' }, + ], + expected: [ + 'retrieve_user', + 'get_user_profile', + 'update_user_role_permissions', + 'get_organization_settings', + ], + }, + { + name: 'complex filter: user resources with no tags', + filters: [ + { type: 'resource', op: 'include', value: 'user.profile' }, + { type: 'tag', op: 'exclude', value: 'admin' }, + ], + expected: ['create_user_profile', 'get_user_profile'], + }, + { + name: 'complex filter: user resources and tags', + filters: [ + { type: 'resource', op: 'include', value: 'user.profile' }, + { type: 'tag', op: 'include', value: 'admin' }, + ], + expected: [ + 'retrieve_user', + 'create_user_profile', + 'get_user_profile', + 'update_user_role_permissions', + 'get_organization_settings', + ], + }, + ]; + + tests.forEach((test) => { + it(`filters by ${test.name}`, () => { + const filtered = query(test.filters, endpoints); + expect(filtered.map((e) => e.tool.name)).toEqual(test.expected); + }); + }); +}); + +function endpoint({ + resource, + operation, + tags, + toolName, +}: { + resource: string; + operation: Metadata['operation']; + tags: string[]; + toolName: string; +}): Endpoint { + return { + metadata: { + resource, + operation, + tags, + }, + tool: { name: toolName, inputSchema: { type: 'object', properties: {} } }, + handler: jest.fn(), + }; +} diff --git a/packages/mcp-server/tsc-multi.json b/packages/mcp-server/tsc-multi.json new file mode 100644 index 0000000..4facad5 --- /dev/null +++ b/packages/mcp-server/tsc-multi.json @@ -0,0 +1,7 @@ +{ + "targets": [ + { "extname": ".js", "module": "commonjs" }, + { "extname": ".mjs", "module": "esnext" } + ], + "projects": ["tsconfig.build.json"] +} diff --git a/packages/mcp-server/tsconfig.build.json b/packages/mcp-server/tsconfig.build.json new file mode 100644 index 0000000..f0bcedc --- /dev/null +++ b/packages/mcp-server/tsconfig.build.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "include": ["dist/src"], + "exclude": [], + "compilerOptions": { + "rootDir": "./dist/src", + "paths": { + "beeper/desktop-api-typescript-mcp/*": ["dist/src/*"], + "beeper/desktop-api-typescript-mcp": ["dist/src/index.ts"] + }, + "noEmit": false, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "pretty": true, + "sourceMap": true + } +} diff --git a/packages/mcp-server/tsconfig.dist-src.json b/packages/mcp-server/tsconfig.dist-src.json new file mode 100644 index 0000000..e9f2d70 --- /dev/null +++ b/packages/mcp-server/tsconfig.dist-src.json @@ -0,0 +1,11 @@ +{ + // this config is included in the published src directory to prevent TS errors + // from appearing when users go to source, and VSCode opens the source .ts file + // via declaration maps + "include": ["index.ts"], + "compilerOptions": { + "target": "es2015", + "lib": ["DOM"], + "moduleResolution": "node" + } +} diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json new file mode 100644 index 0000000..e7c48ad --- /dev/null +++ b/packages/mcp-server/tsconfig.json @@ -0,0 +1,37 @@ +{ + "include": ["src", "tests", "examples"], + "exclude": [], + "compilerOptions": { + "target": "es2020", + "lib": ["es2020"], + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "baseUrl": "./", + "paths": { + "beeper/desktop-api-typescript-mcp/*": ["src/*"], + "beeper/desktop-api-typescript-mcp": ["src/index.ts"] + }, + "noEmit": true, + + "resolveJsonModule": true, + + "forceConsistentCasingInFileNames": true, + + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "alwaysStrict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + + "skipLibCheck": true + } +} diff --git a/packages/mcp-server/yarn.lock b/packages/mcp-server/yarn.lock new file mode 100644 index 0000000..707a2de --- /dev/null +++ b/packages/mcp-server/yarn.lock @@ -0,0 +1,3606 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.2.tgz#4183f9e642fd84e74e3eea7ffa93a412e3b102c9" + integrity sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.1.tgz#89de51e86bd12246003e3524704c49541b16c3e6" + integrity sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.1" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helpers" "^7.27.1" + "@babel/parser" "^7.27.1" + "@babel/template" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.27.1", "@babel/generator@^7.7.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.1.tgz#862d4fad858f7208edd487c28b58144036b76230" + integrity sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w== + dependencies: + "@babel/parser" "^7.27.1" + "@babel/types" "^7.27.1" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.27.1": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-transforms@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz#e1663b8b71d2de948da5c4fb2a20ca4f3ec27a6f" + integrity sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helpers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.1.tgz#ffc27013038607cdba3288e692c3611c06a18aa4" + integrity sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ== + dependencies: + "@babel/template" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.1", "@babel/parser@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.2.tgz#577518bedb17a2ce4212afd052e01f7df0941127" + integrity sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/template@^7.27.1", "@babel/template@^7.3.3": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.1.tgz#4db772902b133bbddd1c4f7a7ee47761c1b9f291" + integrity sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.1" + "@babel/parser" "^7.27.1" + "@babel/template" "^7.27.1" + "@babel/types" "^7.27.1" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.3.3": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560" + integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@cloudflare/cabidela@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@cloudflare/cabidela/-/cabidela-0.2.4.tgz#9a3e9212e636a24d796a8f16741c24885b326a1a" + integrity sha512-u/1OwwqfcMvjmUFOcb6QtFzVVGpncHJxwl254wjzp0JC5CUlBkV6r5BbRrHI5ZYJEAgu8NeeorirxngmMFPZjQ== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + dependencies: + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@modelcontextprotocol/sdk@^1.11.5": + version "1.17.3" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz#cf92354220f0183d28179e96a9bf3a8f6d3211ae" + integrity sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg== + dependencies: + ajv "^6.12.6" + content-type "^1.0.5" + cors "^2.8.5" + cross-spawn "^7.0.5" + eventsource "^3.0.2" + eventsource-parser "^3.0.0" + express "^5.0.1" + express-rate-limit "^7.5.0" + pkce-challenge "^5.0.0" + raw-body "^3.0.0" + zod "^3.23.8" + zod-to-json-schema "^3.24.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgr/core@^0.2.3": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c" + integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@ts-morph/common@~0.20.0": + version "0.20.0" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.20.0.tgz#3f161996b085ba4519731e4d24c35f6cba5b80af" + integrity sha512-7uKjByfbPpwuzkstL3L5MQyuXPSKdoNG93Fmi2JoDcTf3pEP731JdRFAduRVkOs8oqxPsXKA+ScrWkdQ8t/I+Q== + dependencies: + fast-glob "^3.2.12" + minimatch "^7.4.3" + mkdirp "^2.1.6" + path-browserify "^1.0.1" + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/babel__core@^7.1.14": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.7.tgz#968cdc2366ec3da159f61166428ee40f370e56c2" + integrity sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng== + dependencies: + "@babel/types" "^7.20.7" + +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^5.0.0": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6" + integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "*" + +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.4.0": + version "29.5.14" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" + integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node@*": + version "22.15.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.17.tgz#355ccec95f705b664e4332bb64a7f07db30b7055" + integrity sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw== + dependencies: + undici-types "~6.21.0" + +"@types/qs@*": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.5" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@8.31.1": + version "8.31.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz#62f1befe59647524994e89de4516d8dcba7a850a" + integrity sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.31.1" + "@typescript-eslint/type-utils" "8.31.1" + "@typescript-eslint/utils" "8.31.1" + "@typescript-eslint/visitor-keys" "8.31.1" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^2.0.1" + +"@typescript-eslint/parser@8.31.1": + version "8.31.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.31.1.tgz#e9b0ccf30d37dde724ee4d15f4dbc195995cce1b" + integrity sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q== + dependencies: + "@typescript-eslint/scope-manager" "8.31.1" + "@typescript-eslint/types" "8.31.1" + "@typescript-eslint/typescript-estree" "8.31.1" + "@typescript-eslint/visitor-keys" "8.31.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@8.31.1": + version "8.31.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz#1eb52e76878f545e4add142e0d8e3e97e7aa443b" + integrity sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw== + dependencies: + "@typescript-eslint/types" "8.31.1" + "@typescript-eslint/visitor-keys" "8.31.1" + +"@typescript-eslint/type-utils@8.31.1": + version "8.31.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz#be0f438fb24b03568e282a0aed85f776409f970c" + integrity sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA== + dependencies: + "@typescript-eslint/typescript-estree" "8.31.1" + "@typescript-eslint/utils" "8.31.1" + debug "^4.3.4" + ts-api-utils "^2.0.1" + +"@typescript-eslint/types@8.31.1": + version "8.31.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.1.tgz#478ed6f7e8aee1be7b63a60212b6bffe1423b5d4" + integrity sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ== + +"@typescript-eslint/typescript-estree@8.31.1": + version "8.31.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz#37792fe7ef4d3021c7580067c8f1ae66daabacdf" + integrity sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag== + dependencies: + "@typescript-eslint/types" "8.31.1" + "@typescript-eslint/visitor-keys" "8.31.1" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.0.1" + +"@typescript-eslint/utils@8.31.1": + version "8.31.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.1.tgz#5628ea0393598a0b2f143d0fc6d019f0dee9dd14" + integrity sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.31.1" + "@typescript-eslint/types" "8.31.1" + "@typescript-eslint/typescript-estree" "8.31.1" + +"@typescript-eslint/visitor-keys@8.31.1": + version "8.31.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz#6742b0e3ba1e0c1e35bdaf78c03e759eb8dd8e75" + integrity sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw== + dependencies: + "@typescript-eslint/types" "8.31.1" + eslint-visitor-keys "^4.2.0" + +"@ungap/structured-clone@^1.2.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1, acorn@^8.9.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^6.12.4, ajv@^6.12.6: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +async@^3.2.3: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" + integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +body-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" + integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.6.3" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0: + version "4.24.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.5.tgz#aa0f5b8560fe81fde84c6dcb38f759bafba0e11b" + integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== + dependencies: + caniuse-lite "^1.0.30001716" + electron-to-chromium "^1.5.149" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + +bs-logger@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.1.2, bytes@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001716: + version "1.0.30001717" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz#5d9fec5ce09796a1893013825510678928aca129" + integrity sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw== + +chalk@^4.0.0, chalk@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cjs-module-lexer@^1.0.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" + integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +code-block-writer@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770" + integrity sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +dedent@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.6.0.tgz#79d52d6389b1ffa67d2bcef59ba51847a9d503b2" + integrity sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +depd@2.0.0, depd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +ejs@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.5.149: + version "1.5.151" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz#5edd6c17e1b2f14b4662c41b9379f96cc8c2bb7c" + integrity sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-prettier@^5.0.1: + version "5.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz#54d4748904e58eaf1ffe26c4bffa4986ca7f952b" + integrity sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.11.0" + +eslint-plugin-unused-imports@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz#63a98c9ad5f622cd9f830f70bc77739f25ccfe0d" + integrity sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ== + dependencies: + eslint-rule-composer "^0.3.0" + +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@^8.49.0: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventsource-parser@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.3.tgz#e9af1d40b77e6268cdcbc767321e8b9f066adea8" + integrity sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA== + +eventsource-parser@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.1.tgz#5e358dba9a55ba64ca90da883c4ca35bd82467bd" + integrity sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA== + +eventsource@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +express-rate-limit@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.0.tgz#6a67990a724b4fbbc69119419feef50c51e8b28f" + integrity sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg== + +express@^5.0.1, express@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" + integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.0" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-glob@^3.2.12, fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" + integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-errors@2.0.0, http-errors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ignore@^5.2.0, ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-instrument@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jake@^10.8.5: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +jest-util@^29.0.0, jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.4.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +"jq-web@https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz": + version "0.8.6" + resolved "https://github.com/stainless-api/jq-web/releases/download/v0.8.6/jq-web.tar.gz#14d0e126987736e82e964d675c3838b5944faa6f" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@^1.1.1, make-error@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== + dependencies: + mime-db "^1.54.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^7.4.3: + version "7.4.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb" + integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19" + integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-all@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-all/-/p-all-3.0.0.tgz#077c023c37e75e760193badab2bad3ccd5782bfb" + integrity sha512-qUZbvbBFVXm6uJ7U/WDiO0fv6waBMbjlCm4E66oZdRR+egswICarIdHyVSZZHudH8T5SF8x/JG0q0duFzPnlBw== + dependencies: + p-map "^4.0.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== + +pkce-challenge@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.0.tgz#c3a405cb49e272094a38e890a2b51da0228c4d97" + integrity sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^3.0.0: + version "3.5.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5" + integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw== + +pretty-format@^29.0.0, pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +proxy-addr@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + +qs@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" + integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== + +resolve@^1.20.0: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.1: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== + +send@^1.1.0, send@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" + integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== + dependencies: + debug "^4.3.5" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.0" + mime-types "^3.0.1" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" + +serve-static@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" + integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@2.0.1, statuses@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-to-stream@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/string-to-stream/-/string-to-stream-3.0.1.tgz#480e6fb4d5476d31cb2221f75307a5dcb6638a42" + integrity sha512-Hl092MV3USJuUCC6mfl9sPzGloA3K5VwdIeJjYIkXY/8K+mUvaeEabWJgArp+xXrsWxCajeT2pc4axbVhIZJyg== + dependencies: + readable-stream "^3.4.0" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +superstruct@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.4.tgz#0adb99a7578bd2f1c526220da6571b2d485d91ca" + integrity sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +synckit@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.4.tgz#48972326b59723fc15b8d159803cf8302b545d59" + integrity sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ== + dependencies: + "@pkgr/core" "^0.2.3" + tslib "^2.8.1" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-api-utils@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== + +ts-jest@^29.1.0: + version "29.3.2" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.3.2.tgz#0576cdf0a507f811fe73dcd16d135ce89f8156cb" + integrity sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug== + dependencies: + bs-logger "^0.2.6" + ejs "^3.1.10" + fast-json-stable-stringify "^2.1.0" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "^4.1.2" + make-error "^1.3.6" + semver "^7.7.1" + type-fest "^4.39.1" + yargs-parser "^21.1.1" + +ts-morph@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-19.0.0.tgz#43e95fb0156c3fe3c77c814ac26b7d0be2f93169" + integrity sha512-D6qcpiJdn46tUqV45vr5UGM2dnIEuTGNxVhg0sk5NX11orcouwj6i1bMqZIz2mZTZB1Hcgy7C3oEVhAT+f6mbQ== + dependencies: + "@ts-morph/common" "~0.20.0" + code-block-writer "^12.0.0" + +ts-node@^10.5.0: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +"tsc-multi@https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz": + version "1.1.8" + resolved "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz#f544b359b8f05e607771ffacc280e58201476b04" + dependencies: + debug "^4.3.7" + fast-glob "^3.3.2" + get-stdin "^8.0.0" + p-all "^3.0.0" + picocolors "^1.1.1" + signal-exit "^3.0.7" + string-to-stream "^3.0.1" + superstruct "^1.0.4" + tslib "^2.8.1" + yargs "^17.7.2" + +tsconfig-paths@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^4.39.1: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + +type-is@^2.0.0, type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + +typescript@5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^9.0.1: + version "9.3.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +vary@^1, vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod-to-json-schema@^3.24.1, zod-to-json-schema@^3.24.5: + version "3.24.5" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3" + integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== + +zod-validation-error@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.1.tgz#a105723eb40299578a6a38cb86647068f6d005b1" + integrity sha512-F3rdaCOHs5ViJ5YTz5zzRtfkQdMdIeKudJAoxy7yB/2ZMEHw73lmCAcQw11r7++20MyGl4WV59EVh7A9rNAyog== + +zod@^3.23.8: + version "3.24.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.4.tgz#e2e2cca5faaa012d76e527d0d36622e0a90c315f" + integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg== + +zod@^3.25.20: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..b190980 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,73 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "node", + "extra-files": [ + "src/version.ts", + "README.md", + "packages/mcp-server/yarn.lock", + { + "type": "json", + "path": "packages/mcp-server/package.json", + "jsonpath": "$.version" + } + ] +} diff --git a/scripts/build b/scripts/build index a5caef7..38a23ed 100755 --- a/scripts/build +++ b/scripts/build @@ -8,7 +8,7 @@ node scripts/utils/check-version.cjs # Build into dist and will publish the package from there, # so that src/resources/foo.ts becomes /resources/foo.js -# This way importing from `"beeper-desktop-api/resources/foo"` works +# This way importing from `"desktop-api-typescript/resources/foo"` works # even with `"moduleResolution": "node"` rm -rf dist; mkdir dist @@ -42,10 +42,16 @@ node scripts/utils/postprocess-files.cjs # make sure that nothing crashes when we require the output CJS or # import the output ESM -(cd dist && node -e 'require("beeper-desktop-api")') -(cd dist && node -e 'import("beeper-desktop-api")' --input-type=module) +(cd dist && node -e 'require("desktop-api-typescript")') +(cd dist && node -e 'import("desktop-api-typescript")' --input-type=module) if [ -e ./scripts/build-deno ] then ./scripts/build-deno fi +# build all sub-packages +for dir in packages/*; do + if [ -d "$dir" ]; then + (cd "$dir" && yarn install && yarn build) + fi +done diff --git a/scripts/build-all b/scripts/build-all new file mode 100755 index 0000000..4e5ac01 --- /dev/null +++ b/scripts/build-all @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +# build-all is deprecated, use build instead + +bash ./scripts/build diff --git a/scripts/publish-packages.ts b/scripts/publish-packages.ts new file mode 100644 index 0000000..50e93fe --- /dev/null +++ b/scripts/publish-packages.ts @@ -0,0 +1,102 @@ +/** + * Called from the `create-releases.yml` workflow with the output + * of the release please action as the first argument. + * + * Example JSON input: + * + * ```json + { + "releases_created": "true", + "release_created": "true", + "id": "137967744", + "name": "sdk: v0.14.5", + "tag_name": "sdk-v0.14.5", + "sha": "7cc2ba5c694e76a117f731d4cf0b06f8b8361f2e", + "body": "## 0.14.5 (2024-01-22)\n\n...", + "html_url": "https://github.com/$org/$repo/releases/tag/sdk-v0.14.5", + "draft": "false", + "upload_url": "https://uploads.github.com/repos/$org/$repo/releases/137967744/assets{?name,label}", + "path": ".", + "version": "0.14.5", + "major": "0", + "minor": "14", + "patch": "5", + "packages/additional-sdk--release_created": "true", + "packages/additional-sdk--id": "137967756", + "packages/additional-sdk--name": "additional-sdk: v0.5.2", + "packages/additional-sdk--tag_name": "additional-sdk-v0.5.2", + "packages/additional-sdk--sha": "7cc2ba5c694e76a117f731d4cf0b06f8b8361f2e", + "packages/additional-sdk--body": "## 0.5.2 (2024-01-22)\n\n...", + "packages/additional-sdk--html_url": "https://github.com/$org/$repo/releases/tag/additional-sdk-v0.5.2", + "packages/additional-sdk--draft": "false", + "packages/additional-sdk--upload_url": "https://uploads.github.com/repos/$org/$repo/releases/137967756/assets{?name,label}", + "packages/additional-sdk--path": "packages/additional-sdk", + "packages/additional-sdk--version": "0.5.2", + "packages/additional-sdk--major": "0", + "packages/additional-sdk--minor": "5", + "packages/additional-sdk--patch": "2", + "paths_released": "[\".\",\"packages/additional-sdk\"]" + } + ``` + */ + +import { execSync } from 'child_process'; +import path from 'path'; + +function main() { + const data = process.argv[2] ?? process.env['DATA']; + if (!data) { + throw new Error(`Usage: publish-packages.ts '{"json": "obj"}'`); + } + + const rootDir = path.join(__dirname, '..'); + console.log('root dir', rootDir); + console.log(`publish-packages called with ${data}`); + + const outputs = JSON.parse(data); + + const rawPaths = outputs.paths_released; + + if (!rawPaths) { + console.error(JSON.stringify(outputs, null, 2)); + throw new Error('Expected outputs to contain a truthy `paths_released` property'); + } + if (typeof rawPaths !== 'string') { + console.error(JSON.stringify(outputs, null, 2)); + throw new Error('Expected outputs `paths_released` property to be a JSON string'); + } + + const paths = JSON.parse(rawPaths); + if (!Array.isArray(paths)) { + console.error(JSON.stringify(outputs, null, 2)); + throw new Error('Expected outputs `paths_released` property to be an array'); + } + if (!paths.length) { + console.error(JSON.stringify(outputs, null, 2)); + throw new Error('Expected outputs `paths_released` property to contain at least one entry'); + } + + const publishScriptPath = path.join(rootDir, 'bin', 'publish-npm'); + console.log('Using publish script at', publishScriptPath); + + console.log('Ensuring root package is built'); + console.log(`$ yarn build`); + execSync(`yarn build`, { cwd: rootDir, encoding: 'utf8', stdio: 'inherit' }); + + for (const relPackagePath of paths) { + console.log('\n'); + + const packagePath = path.join(rootDir, relPackagePath); + console.log(`Publishing in directory: ${packagePath}`); + + console.log(`$ yarn install`); + execSync(`yarn install`, { cwd: packagePath, encoding: 'utf8', stdio: 'inherit' }); + + console.log(`$ bash ${publishScriptPath}`); + execSync(`bash ${publishScriptPath}`, { cwd: packagePath, encoding: 'utf8', stdio: 'inherit' }); + } + + console.log('Finished publishing packages'); +} + +main(); diff --git a/scripts/utils/make-dist-package-json.cjs b/scripts/utils/make-dist-package-json.cjs index 7c24f56..4d6634e 100644 --- a/scripts/utils/make-dist-package-json.cjs +++ b/scripts/utils/make-dist-package-json.cjs @@ -12,6 +12,14 @@ processExportMap(pkgJson.exports); for (const key of ['types', 'main', 'module']) { if (typeof pkgJson[key] === 'string') pkgJson[key] = pkgJson[key].replace(/^(\.\/)?dist\//, './'); } +// Fix bin paths if present +if (pkgJson.bin) { + for (const key in pkgJson.bin) { + if (typeof pkgJson.bin[key] === 'string') { + pkgJson.bin[key] = pkgJson.bin[key].replace(/^(\.\/)?dist\//, './'); + } + } +} delete pkgJson.devDependencies; delete pkgJson.scripts.prepack; diff --git a/src/client.ts b/src/client.ts index 63b85f9..8d480d1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -13,53 +13,84 @@ import * as Shims from './internal/shims'; import * as Opts from './internal/request-options'; import * as qs from './internal/qs'; import { VERSION } from './version'; -import * as Errors from './core/error'; -import * as Uploads from './core/uploads'; -import * as API from './resources/index'; -import { APIPromise } from './core/api-promise'; -import { ArchiveChat, ArchiveChatArchiveParams } from './resources/archive-chat'; -import { ClearChatReminder, ClearChatReminderClearParams } from './resources/clear-chat-reminder'; -import { DraftMessage, DraftMessageCreateParams } from './resources/draft-message'; -import { FindChatListParams, FindChatListResponse, FindChats } from './resources/find-chats'; -import { BaseResponse, FocusApp, FocusAppOpenParams } from './resources/focus-app'; -import { GetAccountListResponse, GetAccounts } from './resources/get-accounts'; -import { GetChat, GetChatRetrieveParams, GetChatRetrieveResponse, User } from './resources/get-chat'; +import * as Errors from 'beeper/desktop-api-typescript/core/error'; +import * as Pagination from 'beeper/desktop-api-typescript/core/pagination'; import { - GetLinkToChat, - GetLinkToChatCreateParams, - GetLinkToChatCreateResponse, -} from './resources/get-link-to-chat'; -import { OAuth, OAuthRetrieveUserInfoResponse, OAuthRevokeTokenParams } from './resources/oauth'; + AbstractPage, + type CursorIDParams, + CursorIDResponse, +} from 'beeper/desktop-api-typescript/core/pagination'; +import * as Uploads from 'beeper/desktop-api-typescript/core/uploads'; +import * as API from 'beeper/desktop-api-typescript/resources/index'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +import { Account, Accounts, AccountsResponse } from 'beeper/desktop-api-typescript/resources/accounts'; +import { App, AppFocusParams, FocusRequest } from 'beeper/desktop-api-typescript/resources/app'; import { - SearchMessageSearchParams, - SearchMessageSearchResponse, - SearchMessages, -} from './resources/search-messages'; -import { SendMessage, SendMessageSendParams, SendMessageSendResponse } from './resources/send-message'; -import { SetChatReminder, SetChatReminderCreateParams } from './resources/set-chat-reminder'; -import { type Fetch } from './internal/builtin-types'; -import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers'; -import { FinalRequestOptions, RequestOptions } from './internal/request-options'; -import { readEnv } from './internal/utils/env'; + ArchiveRequest, + Chat, + ChatArchiveParams, + ChatFindParams, + ChatGetLinkParams, + ChatRetrieveParams, + Chats, + ChatsCursorID, + FindChatsRequest, + FindChatsResponse, + GetChatRequest, + GetChatResponse, + LinkRequest, + LinkResponse, +} from 'beeper/desktop-api-typescript/resources/chats'; +import { + DraftRequest, + Message, + MessageDraftParams, + MessageSearchParams, + MessageSendParams, + Messages, + MessagesCursorID, + SearchRequest, + SearchResponse, + SendRequest, + SendResponse, +} from 'beeper/desktop-api-typescript/resources/messages'; +import { + OAuth, + OAuthRevokeTokenParams, + RevokeRequest, + UserInfo, +} from 'beeper/desktop-api-typescript/resources/oauth'; +import { + ClearReminderRequest, + ReminderClearParams, + ReminderSetParams, + Reminders, + SetReminderRequest, +} from 'beeper/desktop-api-typescript/resources/reminders'; +import { type Fetch } from 'beeper/desktop-api-typescript/internal/builtin-types'; +import { isRunningInBrowser } from 'beeper/desktop-api-typescript/internal/detect-platform'; +import { HeadersLike, NullableHeaders, buildHeaders } from 'beeper/desktop-api-typescript/internal/headers'; +import { FinalRequestOptions, RequestOptions } from 'beeper/desktop-api-typescript/internal/request-options'; +import { readEnv } from 'beeper/desktop-api-typescript/internal/utils/env'; import { type LogLevel, type Logger, formatRequestDetails, loggerFor, parseLogLevel, -} from './internal/utils/log'; -import { isEmptyObj } from './internal/utils/values'; +} from 'beeper/desktop-api-typescript/internal/utils/log'; +import { isEmptyObj } from 'beeper/desktop-api-typescript/internal/utils/values'; export interface ClientOptions { /** - * Bearer token authentication. Token can be obtained either by creating it in-app or through OAuth2 authorization code flow. + * Access token - either created in-app or obtained via OAuth2 authorization code flow */ - apiKey?: string | null | undefined; + accessToken?: string | null | undefined; /** * Override the default base URL for the API, e.g., "https://api.example.com/v2/" * - * Defaults to process.env['BEEPER_DESKTOP_API_BASE_URL']. + * Defaults to process.env['BEEPER-DESKTOP_BASE_URL']. */ baseURL?: string | null | undefined; @@ -90,7 +121,7 @@ export interface ClientOptions { * The maximum number of times that the client will retry a request in case of a * temporary failure, like a network error or a 5XX error from the server. * - * @default 2 + * @default 3 */ maxRetries?: number | undefined; @@ -110,10 +141,16 @@ export interface ClientOptions { */ defaultQuery?: Record | undefined; + /** + * By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers. + * Only set this option to `true` if you understand the risks and have appropriate mitigations in place. + */ + dangerouslyAllowBrowser?: boolean | undefined; + /** * Set the log level. * - * Defaults to process.env['BEEPER_DESKTOP_API_LOG'] or 'warn' if it isn't set. + * Defaults to process.env['BEEPER-DESKTOP_LOG'] or 'warn' if it isn't set. */ logLevel?: LogLevel | undefined; @@ -126,10 +163,10 @@ export interface ClientOptions { } /** - * API Client for interfacing with the Beeper Desktop API API. + * API Client for interfacing with the Beeper Desktop API. */ -export class BeeperDesktopAPI { - apiKey: string | null; +export class BeeperDesktop { + accessToken: string | null; baseURL: string; maxRetries: number; @@ -144,46 +181,53 @@ export class BeeperDesktopAPI { private _options: ClientOptions; /** - * API Client for interfacing with the Beeper Desktop API API. + * API Client for interfacing with the Beeper Desktop API. * - * @param {string | null | undefined} [opts.apiKey=process.env['BEEPER_DESKTOP_API_API_KEY'] ?? null] - * @param {string} [opts.baseURL=process.env['BEEPER_DESKTOP_API_BASE_URL'] ?? http://localhost:23374] - Override the default base URL for the API. - * @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out. + * @param {string | null | undefined} [opts.accessToken=process.env['BEEPER_ACCESS_TOKEN'] ?? null] + * @param {string} [opts.baseURL=process.env['BEEPER-DESKTOP_BASE_URL'] ?? http://localhost:23374] - Override the default base URL for the API. + * @param {number} [opts.timeout=30 seconds] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out. * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls. * @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation. - * @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request. + * @param {number} [opts.maxRetries=3] - The maximum number of times the client will retry a request. * @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API. * @param {Record} opts.defaultQuery - Default query parameters to include with every request to the API. + * @param {boolean} [opts.dangerouslyAllowBrowser=false] - By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers. */ constructor({ - baseURL = readEnv('BEEPER_DESKTOP_API_BASE_URL'), - apiKey = readEnv('BEEPER_DESKTOP_API_API_KEY') ?? null, + baseURL = readEnv('BEEPER-DESKTOP_BASE_URL'), + accessToken = readEnv('BEEPER_ACCESS_TOKEN') ?? null, ...opts }: ClientOptions = {}) { const options: ClientOptions = { - apiKey, + accessToken, ...opts, baseURL: baseURL || `http://localhost:23374`, }; + if (!options.dangerouslyAllowBrowser && isRunningInBrowser()) { + throw new Errors.BeeperDesktopError( + 'This is disabled by default, as it risks exposing your secret API credentials to attackers.\nIf you understand the risks and have appropriate mitigations in place,\nyou can set the `dangerouslyAllowBrowser` option to `true`, e.g.,\n\nnew BeeperDesktop({ dangerouslyAllowBrowser: true })', + ); + } + this.baseURL = options.baseURL!; - this.timeout = options.timeout ?? BeeperDesktopAPI.DEFAULT_TIMEOUT /* 1 minute */; + this.timeout = options.timeout ?? BeeperDesktop.DEFAULT_TIMEOUT /* 30 seconds */; this.logger = options.logger ?? console; const defaultLogLevel = 'warn'; // Set default logLevel early so that we can log a warning in parseLogLevel. this.logLevel = defaultLogLevel; this.logLevel = parseLogLevel(options.logLevel, 'ClientOptions.logLevel', this) ?? - parseLogLevel(readEnv('BEEPER_DESKTOP_API_LOG'), "process.env['BEEPER_DESKTOP_API_LOG']", this) ?? + parseLogLevel(readEnv('BEEPER-DESKTOP_LOG'), "process.env['BEEPER-DESKTOP_LOG']", this) ?? defaultLogLevel; this.fetchOptions = options.fetchOptions; - this.maxRetries = options.maxRetries ?? 2; + this.maxRetries = options.maxRetries ?? 3; this.fetch = options.fetch ?? Shims.getDefaultFetch(); this.#encoder = Opts.FallbackEncoder; this._options = options; - this.apiKey = apiKey; + this.accessToken = accessToken; } /** @@ -199,7 +243,7 @@ export class BeeperDesktopAPI { logLevel: this.logLevel, fetch: this.fetch, fetchOptions: this.fetchOptions, - apiKey: this.apiKey, + accessToken: this.accessToken, ...options, }); return client; @@ -225,10 +269,10 @@ export class BeeperDesktopAPI { } protected async bearerAuth(opts: FinalRequestOptions): Promise { - if (this.apiKey == null) { + if (this.accessToken == null) { return undefined; } - return buildHeaders([{ Authorization: `Bearer ${this.apiKey}` }]); + return buildHeaders([{ Authorization: `Bearer ${this.accessToken}` }]); } protected async oauth2Auth(opts: FinalRequestOptions): Promise { @@ -236,7 +280,7 @@ export class BeeperDesktopAPI { } protected stringifyQuery(query: Record): string { - return qs.stringify(query, { arrayFormat: 'comma' }); + return qs.stringify(query, { arrayFormat: 'repeat' }); } private getUserAgent(): string { @@ -491,6 +535,25 @@ export class BeeperDesktopAPI { return { response, options, controller, requestLogID, retryOfRequestLogID, startTime }; } + getAPIList = Pagination.AbstractPage>( + path: string, + Page: new (...args: any[]) => PageClass, + opts?: RequestOptions, + ): Pagination.PagePromise { + return this.requestAPIList(Page, { method: 'get', path, ...opts }); + } + + requestAPIList< + Item = unknown, + PageClass extends Pagination.AbstractPage = Pagination.AbstractPage, + >( + Page: new (...args: ConstructorParameters) => PageClass, + options: FinalRequestOptions, + ): Pagination.PagePromise { + const request = this.makeRequest(options, null, undefined); + return new Pagination.PagePromise(this as any as BeeperDesktop, request, Page); + } + async fetchWithTimeout( url: RequestInfo, init: RequestInit | undefined, @@ -589,8 +652,8 @@ export class BeeperDesktopAPI { } private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number { - const initialRetryDelay = 0.5; - const maxRetryDelay = 8.0; + const initialRetryDelay = 1.0; + const maxRetryDelay = 10.0; const numRetries = maxRetries - retriesRemaining; @@ -704,10 +767,10 @@ export class BeeperDesktopAPI { } } - static BeeperDesktopAPI = this; - static DEFAULT_TIMEOUT = 60000; // 1 minute + static BeeperDesktop = this; + static DEFAULT_TIMEOUT = 30000; // 30 seconds - static BeeperDesktopAPIError = Errors.BeeperDesktopAPIError; + static BeeperDesktopError = Errors.BeeperDesktopError; static APIError = Errors.APIError; static APIConnectionError = Errors.APIConnectionError; static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError; @@ -723,92 +786,98 @@ export class BeeperDesktopAPI { static toFile = Uploads.toFile; - getAccounts: API.GetAccounts = new API.GetAccounts(this); - focusApp: API.FocusApp = new API.FocusApp(this); - draftMessage: API.DraftMessage = new API.DraftMessage(this); - getLinkToChat: API.GetLinkToChat = new API.GetLinkToChat(this); - getChat: API.GetChat = new API.GetChat(this); - findChats: API.FindChats = new API.FindChats(this); - searchMessages: API.SearchMessages = new API.SearchMessages(this); - sendMessage: API.SendMessage = new API.SendMessage(this); - archiveChat: API.ArchiveChat = new API.ArchiveChat(this); - setChatReminder: API.SetChatReminder = new API.SetChatReminder(this); - clearChatReminder: API.ClearChatReminder = new API.ClearChatReminder(this); + /** + * Manage and list connected messaging accounts + */ + accounts: API.Accounts = new API.Accounts(this); + /** + * Control the Beeper Desktop application + */ + app: API.App = new API.App(this); + /** + * Send, draft, and search messages across all chat networks + */ + messages: API.Messages = new API.Messages(this); + /** + * Manage chats, conversations, and threads + */ + chats: API.Chats = new API.Chats(this); + /** + * Set and clear reminders for chats + */ + reminders: API.Reminders = new API.Reminders(this); + /** + * OAuth2 authentication and token management + */ oauth: API.OAuth = new API.OAuth(this); } -BeeperDesktopAPI.GetAccounts = GetAccounts; -BeeperDesktopAPI.FocusApp = FocusApp; -BeeperDesktopAPI.DraftMessage = DraftMessage; -BeeperDesktopAPI.GetLinkToChat = GetLinkToChat; -BeeperDesktopAPI.GetChat = GetChat; -BeeperDesktopAPI.FindChats = FindChats; -BeeperDesktopAPI.SearchMessages = SearchMessages; -BeeperDesktopAPI.SendMessage = SendMessage; -BeeperDesktopAPI.ArchiveChat = ArchiveChat; -BeeperDesktopAPI.SetChatReminder = SetChatReminder; -BeeperDesktopAPI.ClearChatReminder = ClearChatReminder; -BeeperDesktopAPI.OAuth = OAuth; - -export declare namespace BeeperDesktopAPI { - export type RequestOptions = Opts.RequestOptions; - - export { GetAccounts as GetAccounts, type GetAccountListResponse as GetAccountListResponse }; - - export { - FocusApp as FocusApp, - type BaseResponse as BaseResponse, - type FocusAppOpenParams as FocusAppOpenParams, - }; - - export { DraftMessage as DraftMessage, type DraftMessageCreateParams as DraftMessageCreateParams }; +BeeperDesktop.Accounts = Accounts; +BeeperDesktop.App = App; +BeeperDesktop.Messages = Messages; +BeeperDesktop.Chats = Chats; +BeeperDesktop.Reminders = Reminders; +BeeperDesktop.OAuth = OAuth; - export { - GetLinkToChat as GetLinkToChat, - type GetLinkToChatCreateResponse as GetLinkToChatCreateResponse, - type GetLinkToChatCreateParams as GetLinkToChatCreateParams, - }; +export declare namespace BeeperDesktop { + export type RequestOptions = Opts.RequestOptions; - export { - GetChat as GetChat, - type User as User, - type GetChatRetrieveResponse as GetChatRetrieveResponse, - type GetChatRetrieveParams as GetChatRetrieveParams, - }; + export import CursorID = Pagination.CursorID; + export { type CursorIDParams as CursorIDParams, type CursorIDResponse as CursorIDResponse }; - export { - FindChats as FindChats, - type FindChatListResponse as FindChatListResponse, - type FindChatListParams as FindChatListParams, - }; + export { Accounts as Accounts, type Account as Account, type AccountsResponse as AccountsResponse }; - export { - SearchMessages as SearchMessages, - type SearchMessageSearchResponse as SearchMessageSearchResponse, - type SearchMessageSearchParams as SearchMessageSearchParams, - }; + export { App as App, type FocusRequest as FocusRequest, type AppFocusParams as AppFocusParams }; export { - SendMessage as SendMessage, - type SendMessageSendResponse as SendMessageSendResponse, - type SendMessageSendParams as SendMessageSendParams, + Messages as Messages, + type DraftRequest as DraftRequest, + type Message as Message, + type SearchRequest as SearchRequest, + type SearchResponse as SearchResponse, + type SendRequest as SendRequest, + type SendResponse as SendResponse, + type MessagesCursorID as MessagesCursorID, + type MessageDraftParams as MessageDraftParams, + type MessageSearchParams as MessageSearchParams, + type MessageSendParams as MessageSendParams, }; - export { ArchiveChat as ArchiveChat, type ArchiveChatArchiveParams as ArchiveChatArchiveParams }; - export { - SetChatReminder as SetChatReminder, - type SetChatReminderCreateParams as SetChatReminderCreateParams, + Chats as Chats, + type ArchiveRequest as ArchiveRequest, + type Chat as Chat, + type FindChatsRequest as FindChatsRequest, + type FindChatsResponse as FindChatsResponse, + type GetChatRequest as GetChatRequest, + type GetChatResponse as GetChatResponse, + type LinkRequest as LinkRequest, + type LinkResponse as LinkResponse, + type ChatsCursorID as ChatsCursorID, + type ChatRetrieveParams as ChatRetrieveParams, + type ChatArchiveParams as ChatArchiveParams, + type ChatFindParams as ChatFindParams, + type ChatGetLinkParams as ChatGetLinkParams, }; export { - ClearChatReminder as ClearChatReminder, - type ClearChatReminderClearParams as ClearChatReminderClearParams, + Reminders as Reminders, + type ClearReminderRequest as ClearReminderRequest, + type SetReminderRequest as SetReminderRequest, + type ReminderClearParams as ReminderClearParams, + type ReminderSetParams as ReminderSetParams, }; export { OAuth as OAuth, - type OAuthRetrieveUserInfoResponse as OAuthRetrieveUserInfoResponse, + type RevokeRequest as RevokeRequest, + type UserInfo as UserInfo, type OAuthRevokeTokenParams as OAuthRevokeTokenParams, }; + + export type Attachment = API.Attachment; + export type BaseResponse = API.BaseResponse; + export type Error = API.Error; + export type Reaction = API.Reaction; + export type User = API.User; } diff --git a/src/core/api-promise.ts b/src/core/api-promise.ts index f9f111f..abda22c 100644 --- a/src/core/api-promise.ts +++ b/src/core/api-promise.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { type BeeperDesktopAPI } from '../client'; +import { type BeeperDesktop } from 'beeper/desktop-api-typescript/client'; import { type PromiseOrValue } from '../internal/types'; import { APIResponseProps, defaultParseResponse } from '../internal/parse'; @@ -11,13 +11,13 @@ import { APIResponseProps, defaultParseResponse } from '../internal/parse'; */ export class APIPromise extends Promise { private parsedPromise: Promise | undefined; - #client: BeeperDesktopAPI; + #client: BeeperDesktop; constructor( - client: BeeperDesktopAPI, + client: BeeperDesktop, private responsePromise: Promise, private parseResponse: ( - client: BeeperDesktopAPI, + client: BeeperDesktop, props: APIResponseProps, ) => PromiseOrValue = defaultParseResponse, ) { diff --git a/src/core/error.ts b/src/core/error.ts index 816d054..0b3ec08 100644 --- a/src/core/error.ts +++ b/src/core/error.ts @@ -2,13 +2,13 @@ import { castToError } from '../internal/errors'; -export class BeeperDesktopAPIError extends Error {} +export class BeeperDesktopError extends Error {} export class APIError< TStatus extends number | undefined = number | undefined, THeaders extends Headers | undefined = Headers | undefined, TError extends Object | undefined = Object | undefined, -> extends BeeperDesktopAPIError { +> extends BeeperDesktopError { /** HTTP status for the response that caused the error */ readonly status: TStatus; /** HTTP headers for the response that caused the error */ diff --git a/src/core/pagination.ts b/src/core/pagination.ts new file mode 100644 index 0000000..04affca --- /dev/null +++ b/src/core/pagination.ts @@ -0,0 +1,190 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { BeeperDesktopError } from './error'; +import { FinalRequestOptions } from '../internal/request-options'; +import { defaultParseResponse } from '../internal/parse'; +import { type BeeperDesktop } from 'beeper/desktop-api-typescript/client'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +import { type APIResponseProps } from 'beeper/desktop-api-typescript/internal/parse'; +import { maybeObj } from 'beeper/desktop-api-typescript/internal/utils/values'; + +export type PageRequestOptions = Pick; + +export abstract class AbstractPage implements AsyncIterable { + #client: BeeperDesktop; + protected options: FinalRequestOptions; + + protected response: Response; + protected body: unknown; + + constructor(client: BeeperDesktop, response: Response, body: unknown, options: FinalRequestOptions) { + this.#client = client; + this.options = options; + this.response = response; + this.body = body; + } + + abstract nextPageRequestOptions(): PageRequestOptions | null; + + abstract getPaginatedItems(): Item[]; + + hasNextPage(): boolean { + const items = this.getPaginatedItems(); + if (!items.length) return false; + return this.nextPageRequestOptions() != null; + } + + async getNextPage(): Promise { + const nextOptions = this.nextPageRequestOptions(); + if (!nextOptions) { + throw new BeeperDesktopError( + 'No next page expected; please check `.hasNextPage()` before calling `.getNextPage()`.', + ); + } + + return await this.#client.requestAPIList(this.constructor as any, nextOptions); + } + + async *iterPages(): AsyncGenerator { + let page: this = this; + yield page; + while (page.hasNextPage()) { + page = await page.getNextPage(); + yield page; + } + } + + async *[Symbol.asyncIterator](): AsyncGenerator { + for await (const page of this.iterPages()) { + for (const item of page.getPaginatedItems()) { + yield item; + } + } + } +} + +/** + * This subclass of Promise will resolve to an instantiated Page once the request completes. + * + * It also implements AsyncIterable to allow auto-paginating iteration on an unawaited list call, eg: + * + * for await (const item of client.items.list()) { + * console.log(item) + * } + */ +export class PagePromise< + PageClass extends AbstractPage, + Item = ReturnType[number], + > + extends APIPromise + implements AsyncIterable +{ + constructor( + client: BeeperDesktop, + request: Promise, + Page: new (...args: ConstructorParameters) => PageClass, + ) { + super( + client, + request, + async (client, props) => + new Page(client, props.response, await defaultParseResponse(client, props), props.options), + ); + } + + /** + * Allow auto-paginating iteration on an unawaited list call, eg: + * + * for await (const item of client.items.list()) { + * console.log(item) + * } + */ + async *[Symbol.asyncIterator](): AsyncGenerator { + const page = await this; + for await (const item of page) { + yield item; + } + } +} + +export interface CursorIDResponse { + data: Array; + + has_more: boolean; +} + +export interface CursorIDParams { + starting_after?: string | null; + + ending_before?: string | null; + + limit?: number | null; +} + +export class CursorID + extends AbstractPage + implements CursorIDResponse +{ + data: Array; + + has_more: boolean; + + constructor( + client: BeeperDesktop, + response: Response, + body: CursorIDResponse, + options: FinalRequestOptions, + ) { + super(client, response, body, options); + + this.data = body.data || []; + this.has_more = body.has_more || false; + } + + getPaginatedItems(): Item[] { + return this.data ?? []; + } + + override hasNextPage(): boolean { + if (this.has_more === false) { + return false; + } + + return super.hasNextPage(); + } + + nextPageRequestOptions(): PageRequestOptions | null { + const data = this.getPaginatedItems(); + + const isForwards = !( + typeof this.options.query === 'object' && 'ending_before' in (this.options.query || {}) + ); + if (isForwards) { + const id = data[data.length - 1]?.id; + if (!id) { + return null; + } + + return { + ...this.options, + query: { + ...maybeObj(this.options.query), + starting_after: id, + }, + }; + } + + const id = data[0]?.id; + if (!id) { + return null; + } + + return { + ...this.options, + query: { + ...maybeObj(this.options.query), + ending_before: id, + }, + }; + } +} diff --git a/src/core/resource.ts b/src/core/resource.ts index efe3833..aa4e9d0 100644 --- a/src/core/resource.ts +++ b/src/core/resource.ts @@ -1,11 +1,11 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import type { BeeperDesktopAPI } from '../client'; +import type { BeeperDesktop } from '../client'; export abstract class APIResource { - protected _client: BeeperDesktopAPI; + protected _client: BeeperDesktop; - constructor(client: BeeperDesktopAPI) { + constructor(client: BeeperDesktop) { this._client = client; } } diff --git a/src/index.ts b/src/index.ts index 9c6127f..39883d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,13 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export { BeeperDesktopAPI as default } from './client'; +export { BeeperDesktop as default } from './client'; -export { type Uploadable, toFile } from './core/uploads'; -export { APIPromise } from './core/api-promise'; -export { BeeperDesktopAPI, type ClientOptions } from './client'; +export { type Uploadable, toFile } from 'beeper/desktop-api-typescript/core/uploads'; +export { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +export { BeeperDesktop, type ClientOptions } from 'beeper/desktop-api-typescript/client'; +export { PagePromise } from 'beeper/desktop-api-typescript/core/pagination'; export { - BeeperDesktopAPIError, + BeeperDesktopError, APIError, APIConnectionError, APIConnectionTimeoutError, @@ -19,4 +20,4 @@ export { InternalServerError, PermissionDeniedError, UnprocessableEntityError, -} from './core/error'; +} from 'beeper/desktop-api-typescript/core/error'; diff --git a/src/internal/parse.ts b/src/internal/parse.ts index 41a3998..0ca39de 100644 --- a/src/internal/parse.ts +++ b/src/internal/parse.ts @@ -1,8 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import type { FinalRequestOptions } from './request-options'; -import { type BeeperDesktopAPI } from '../client'; -import { formatRequestDetails, loggerFor } from './utils/log'; +import { type BeeperDesktop } from 'beeper/desktop-api-typescript/client'; +import { formatRequestDetails, loggerFor } from 'beeper/desktop-api-typescript/internal/utils/log'; export type APIResponseProps = { response: Response; @@ -13,7 +13,7 @@ export type APIResponseProps = { startTime: number; }; -export async function defaultParseResponse(client: BeeperDesktopAPI, props: APIResponseProps): Promise { +export async function defaultParseResponse(client: BeeperDesktop, props: APIResponseProps): Promise { const { response, requestLogID, retryOfRequestLogID, startTime } = props; const body = await (async () => { // fetch refuses to read the body when the status code is 204. diff --git a/src/internal/request-options.ts b/src/internal/request-options.ts index 2aabf9a..ad5d2bf 100644 --- a/src/internal/request-options.ts +++ b/src/internal/request-options.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { NullableHeaders } from './headers'; +import { NullableHeaders } from 'beeper/desktop-api-typescript/internal/headers'; import type { BodyInit } from './builtin-types'; import type { HTTPMethod, MergedRequestInit } from './types'; @@ -40,7 +40,7 @@ export type RequestOptions = { * The maximum number of times that the client will retry a request in case of a * temporary failure, like a network error or a 5XX error from the server. * - * @default 2 + * @default 3 */ maxRetries?: number; diff --git a/src/internal/shims.ts b/src/internal/shims.ts index 1b2ffc6..42d101e 100644 --- a/src/internal/shims.ts +++ b/src/internal/shims.ts @@ -16,7 +16,7 @@ export function getDefaultFetch(): Fetch { } throw new Error( - '`fetch` is not defined as a global; Either pass `fetch` to the client, `new BeeperDesktopAPI({ fetch })` or polyfill the global, `globalThis.fetch = fetch`', + '`fetch` is not defined as a global; Either pass `fetch` to the client, `new BeeperDesktop({ fetch })` or polyfill the global, `globalThis.fetch = fetch`', ); } diff --git a/src/internal/uploads.ts b/src/internal/uploads.ts index da933bc..a89bf1c 100644 --- a/src/internal/uploads.ts +++ b/src/internal/uploads.ts @@ -1,6 +1,6 @@ import { type RequestOptions } from './request-options'; import type { FilePropertyBag, Fetch } from './builtin-types'; -import type { BeeperDesktopAPI } from '../client'; +import type { BeeperDesktop } from '../client'; import { ReadableStreamFrom } from './shims'; export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView; @@ -74,7 +74,7 @@ export const isAsyncIterable = (value: any): value is AsyncIterable => */ export const maybeMultipartFormRequestOptions = async ( opts: RequestOptions, - fetch: BeeperDesktopAPI | Fetch, + fetch: BeeperDesktop | Fetch, ): Promise => { if (!hasUploadableValue(opts.body)) return opts; @@ -85,7 +85,7 @@ type MultipartFormRequestOptions = Omit & { body: unknow export const multipartFormRequestOptions = async ( opts: MultipartFormRequestOptions, - fetch: BeeperDesktopAPI | Fetch, + fetch: BeeperDesktop | Fetch, ): Promise => { return { ...opts, body: await createForm(opts.body, fetch) }; }; @@ -98,7 +98,7 @@ const supportsFormDataMap = /* @__PURE__ */ new WeakMap> * This function detects if the fetch function provided supports the global FormData object to avoid * confusing error messages later on. */ -function supportsFormData(fetchObject: BeeperDesktopAPI | Fetch): Promise { +function supportsFormData(fetchObject: BeeperDesktop | Fetch): Promise { const fetch: Fetch = typeof fetchObject === 'function' ? fetchObject : (fetchObject as any).fetch; const cached = supportsFormDataMap.get(fetch); if (cached) return cached; @@ -124,7 +124,7 @@ function supportsFormData(fetchObject: BeeperDesktopAPI | Fetch): Promise>( body: T | undefined, - fetch: BeeperDesktopAPI | Fetch, + fetch: BeeperDesktop | Fetch, ): Promise => { if (!(await supportsFormData(fetch))) { throw new TypeError( diff --git a/src/internal/utils/base64.ts b/src/internal/utils/base64.ts index 6831522..0bcdb84 100644 --- a/src/internal/utils/base64.ts +++ b/src/internal/utils/base64.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { BeeperDesktopAPIError } from '../../core/error'; -import { encodeUTF8 } from './bytes'; +import { BeeperDesktopError } from 'beeper/desktop-api-typescript/core/error'; +import { encodeUTF8 } from 'beeper/desktop-api-typescript/internal/utils/bytes'; export const toBase64 = (data: string | Uint8Array | null | undefined): string => { if (!data) return ''; @@ -18,7 +18,7 @@ export const toBase64 = (data: string | Uint8Array | null | undefined): string = return btoa(String.fromCharCode.apply(null, data as any)); } - throw new BeeperDesktopAPIError('Cannot generate base64 string; Expected `Buffer` or `btoa` to be defined'); + throw new BeeperDesktopError('Cannot generate base64 string; Expected `Buffer` or `btoa` to be defined'); }; export const fromBase64 = (str: string): Uint8Array => { @@ -36,5 +36,5 @@ export const fromBase64 = (str: string): Uint8Array => { return buf; } - throw new BeeperDesktopAPIError('Cannot decode base64 string; Expected `Buffer` or `atob` to be defined'); + throw new BeeperDesktopError('Cannot decode base64 string; Expected `Buffer` or `atob` to be defined'); }; diff --git a/src/internal/utils/log.ts b/src/internal/utils/log.ts index 70256e2..8def1b0 100644 --- a/src/internal/utils/log.ts +++ b/src/internal/utils/log.ts @@ -1,8 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { hasOwn } from './values'; -import { type BeeperDesktopAPI } from '../../client'; -import { RequestOptions } from '../request-options'; +import { type BeeperDesktop } from 'beeper/desktop-api-typescript/client'; +import { RequestOptions } from 'beeper/desktop-api-typescript/internal/request-options'; type LogFn = (message: string, ...rest: unknown[]) => void; export type Logger = { @@ -24,7 +24,7 @@ const levelNumbers = { export const parseLogLevel = ( maybeLevel: string | undefined, sourceName: string, - client: BeeperDesktopAPI, + client: BeeperDesktop, ): LogLevel | undefined => { if (!maybeLevel) { return undefined; @@ -60,7 +60,7 @@ const noopLogger = { let cachedLoggers = /* @__PURE__ */ new WeakMap(); -export function loggerFor(client: BeeperDesktopAPI): Logger { +export function loggerFor(client: BeeperDesktop): Logger { const logger = client.logger; const logLevel = client.logLevel ?? 'off'; if (!logger) { diff --git a/src/internal/utils/path.ts b/src/internal/utils/path.ts index 4552ea6..7f2c3f9 100644 --- a/src/internal/utils/path.ts +++ b/src/internal/utils/path.ts @@ -1,4 +1,4 @@ -import { BeeperDesktopAPIError } from '../../core/error'; +import { BeeperDesktopError } from '../../core/error'; /** * Percent-encode everything that isn't safe to have in a path without encoding safe chars. @@ -72,7 +72,7 @@ export const createPathTagFunction = (pathEncoder = encodeURIPath) => return acc + spaces + arrows; }, ''); - throw new BeeperDesktopAPIError( + throw new BeeperDesktopError( `Path parameters result in path with invalid segments:\n${invalidSegments .map((e) => e.error) .join('\n')}\n${path}\n${underline}`, diff --git a/src/internal/utils/values.ts b/src/internal/utils/values.ts index 47db43a..4027e2c 100644 --- a/src/internal/utils/values.ts +++ b/src/internal/utils/values.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { BeeperDesktopAPIError } from '../../core/error'; +import { BeeperDesktopError } from 'beeper/desktop-api-typescript/core/error'; // https://url.spec.whatwg.org/#url-scheme-string const startsWithSchemeRegexp = /^[a-z][a-z0-9+.-]*:/i; @@ -39,7 +39,7 @@ export function isObj(obj: unknown): obj is Record { export const ensurePresent = (value: T | null | undefined): T => { if (value == null) { - throw new BeeperDesktopAPIError(`Expected a value to be given but received ${value} instead.`); + throw new BeeperDesktopError(`Expected a value to be given but received ${value} instead.`); } return value; @@ -47,10 +47,10 @@ export const ensurePresent = (value: T | null | undefined): T => { export const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { - throw new BeeperDesktopAPIError(`${name} must be an integer`); + throw new BeeperDesktopError(`${name} must be an integer`); } if (n < 0) { - throw new BeeperDesktopAPIError(`${name} must be a positive integer`); + throw new BeeperDesktopError(`${name} must be a positive integer`); } return n; }; @@ -59,14 +59,14 @@ export const coerceInteger = (value: unknown): number => { if (typeof value === 'number') return Math.round(value); if (typeof value === 'string') return parseInt(value, 10); - throw new BeeperDesktopAPIError(`Could not coerce ${value} (type: ${typeof value}) into a number`); + throw new BeeperDesktopError(`Could not coerce ${value} (type: ${typeof value}) into a number`); }; export const coerceFloat = (value: unknown): number => { if (typeof value === 'number') return value; if (typeof value === 'string') return parseFloat(value); - throw new BeeperDesktopAPIError(`Could not coerce ${value} (type: ${typeof value}) into a number`); + throw new BeeperDesktopError(`Could not coerce ${value} (type: ${typeof value}) into a number`); }; export const coerceBoolean = (value: unknown): boolean => { diff --git a/src/pagination.ts b/src/pagination.ts new file mode 100644 index 0000000..90bf015 --- /dev/null +++ b/src/pagination.ts @@ -0,0 +1,2 @@ +/** @deprecated Import from ./core/pagination instead */ +export * from './core/pagination'; diff --git a/src/resources/accounts.ts b/src/resources/accounts.ts new file mode 100644 index 0000000..a386e58 --- /dev/null +++ b/src/resources/accounts.ts @@ -0,0 +1,59 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from 'beeper/desktop-api-typescript/core/resource'; +import * as Shared from 'beeper/desktop-api-typescript/resources/shared'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +import { RequestOptions } from 'beeper/desktop-api-typescript/internal/request-options'; + +/** + * Manage and list connected messaging accounts + */ +export class Accounts extends APIResource { + /** + * List connected Beeper accounts available on this device. + * + * - When to use: select account context before account-scoped operations. + * - Scope: only accounts currently Connected on this device are included. Returns: + * connected accounts. + */ + list(options?: RequestOptions): APIPromise { + return this._client.get('/v0/get-accounts', options); + } +} + +/** + * A chat account added to Beeper + */ +export interface Account { + /** + * Chat account added to Beeper. Use this to route account-scoped actions. + */ + accountID: string; + + /** + * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You + * MUST use 'accountID' to perform actions. + */ + network: string; + + /** + * A person on or reachable through Beeper. Values are best-effort and can vary by + * network. + */ + user: Shared.User; +} + +/** + * Response payload for listing connected Beeper accounts. + */ +export interface AccountsResponse { + /** + * Connected accounts the user can act through. Includes accountID, network, and + * user identity. + */ + accounts: Array; +} + +export declare namespace Accounts { + export { type Account as Account, type AccountsResponse as AccountsResponse }; +} diff --git a/src/resources/app.ts b/src/resources/app.ts new file mode 100644 index 0000000..19483ef --- /dev/null +++ b/src/resources/app.ts @@ -0,0 +1,69 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from 'beeper/desktop-api-typescript/core/resource'; +import * as Shared from 'beeper/desktop-api-typescript/resources/shared'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +import { RequestOptions } from 'beeper/desktop-api-typescript/internal/request-options'; + +/** + * Control the Beeper Desktop application + */ +export class App extends APIResource { + /** + * Bring Beeper Desktop to the foreground on this device. Optionally focuses a + * specific chat if chatID is provided. + * + * - When to use: open Beeper, or jump to a specific chat. + * - Constraints: requires Beeper Desktop running locally; no-op in headless + * environments. + * - Idempotent: safe to call repeatedly. Returns an error if chatID is not found. + * Returns: success. + * + * @example + * ```ts + * const baseResponse = await client.app.focus(); + * ``` + */ + focus( + body: AppFocusParams | null | undefined = {}, + options?: RequestOptions, + ): APIPromise { + return this._client.post('/v0/focus-app', { body, ...options }); + } +} + +/** + * Bring Beeper Desktop to the foreground on this device and optionally jump to a + * chat. + */ +export interface FocusRequest { + /** + * Optional Beeper chat ID to focus after bringing the app to foreground. If + * omitted, only foregrounds the app. Required if messageSortKey is present. No-op + * in headless environments. + */ + chatID?: string; + + /** + * Optional message sort key. Jumps to that message in the chat when foregrounding. + */ + messageSortKey?: string; +} + +export interface AppFocusParams { + /** + * Optional Beeper chat ID to focus after bringing the app to foreground. If + * omitted, only foregrounds the app. Required if messageSortKey is present. No-op + * in headless environments. + */ + chatID?: string; + + /** + * Optional message sort key. Jumps to that message in the chat when foregrounding. + */ + messageSortKey?: string; +} + +export declare namespace App { + export { type FocusRequest as FocusRequest, type AppFocusParams as AppFocusParams }; +} diff --git a/src/resources/archive-chat.ts b/src/resources/archive-chat.ts deleted file mode 100644 index a554da7..0000000 --- a/src/resources/archive-chat.ts +++ /dev/null @@ -1,40 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as FocusAppAPI from './focus-app'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class ArchiveChat extends APIResource { - /** - * Archive or unarchive a chat. Set archived=true to move to archive, - * archived=false to move back to inbox - * - * @example - * ```ts - * const baseResponse = await client.archiveChat.archive({ - * chatID: - * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - * }); - * ``` - */ - archive(body: ArchiveChatArchiveParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/archive-chat', { body, ...options }); - } -} - -export interface ArchiveChatArchiveParams { - /** - * The identifier of the chat to archive or unarchive - */ - chatID: string; - - /** - * True to archive, false to unarchive - */ - archived?: boolean; -} - -export declare namespace ArchiveChat { - export { type ArchiveChatArchiveParams as ArchiveChatArchiveParams }; -} diff --git a/src/resources/chats.ts b/src/resources/chats.ts new file mode 100644 index 0000000..102db1e --- /dev/null +++ b/src/resources/chats.ts @@ -0,0 +1,539 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from 'beeper/desktop-api-typescript/core/resource'; +import * as Shared from 'beeper/desktop-api-typescript/resources/shared'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +import { CursorID, type CursorIDParams, PagePromise } from 'beeper/desktop-api-typescript/core/pagination'; +import { RequestOptions } from 'beeper/desktop-api-typescript/internal/request-options'; + +/** + * Manage chats, conversations, and threads + */ +export class Chats extends APIResource { + /** + * Retrieve chat details: metadata, participants (limited), and latest message. + * + * - When to use: fetch a complete view of a chat beyond what search returns. + * - Constraints: not available for iMessage chats ('imsg##'). Participants limited + * by 'maxParticipantCount' (default 20, max 500). Returns: chat details.Agents: + * ALWAYS use linkToChat to make clickable links in your response + * + * @example + * ```ts + * const getChatResponse = await client.chats.retrieve({ + * chatID: + * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + * }); + * ``` + */ + retrieve(query: ChatRetrieveParams, options?: RequestOptions): APIPromise { + return this._client.get('/v0/get-chat', { query, ...options }); + } + + /** + * Archive or unarchive a chat. Set archived=true to move to archive, + * archived=false to move back to inbox + * + * @example + * ```ts + * const baseResponse = await client.chats.archive({ + * chatID: + * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + * }); + * ``` + */ + archive(body: ChatArchiveParams, options?: RequestOptions): APIPromise { + return this._client.post('/v0/archive-chat', { body, ...options }); + } + + /** + * Search and filter conversations across all messaging accounts. + * + * - When to use: browse chats by inbox (primary/low-priority/archive), type, + * unread status, or search terms. + * - Pagination: use cursor + direction for pagination. + * - Performance: provide accountIDs when known for faster filtering. Returns: + * matching chats with pagination. Agents: ALWAYS use linkToChat to make + * clickable links in your response + * + * @example + * ```ts + * // Automatically fetches more pages as needed. + * for await (const chat of client.chats.find()) { + * // ... + * } + * ``` + */ + find( + query: ChatFindParams | null | undefined = {}, + options?: RequestOptions, + ): PagePromise { + return this._client.getAPIList('/v0/find-chats', CursorID, { query, ...options }); + } + + /** + * Generate a deep link to a specific chat or message. This link can be used to + * open the chat directly in the Beeper app. + * + * @example + * ```ts + * const linkResponse = await client.chats.getLink({ + * chatID: + * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + * }); + * ``` + */ + getLink(body: ChatGetLinkParams, options?: RequestOptions): APIPromise { + return this._client.post('/v0/get-link-to-chat', { body, ...options }); + } +} + +export type ChatsCursorID = CursorID; + +export interface ArchiveRequest { + /** + * The identifier of the chat to archive or unarchive + */ + chatID: string; + + /** + * True to archive, false to unarchive + */ + archived?: boolean; +} + +export interface Chat { + /** + * Unique identifier for cursor pagination. + */ + id: string; + + /** + * Beeper account ID this chat belongs to. + */ + accountID: string; + + /** + * Unique identifier of the chat (room/thread ID, same as id). + */ + chatID: string; + + /** + * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You + * MUST use 'accountID' to perform actions. + */ + network: string; + + /** + * Chat participants information. + */ + participants: Chat.Participants; + + /** + * Display title of the chat as computed by the client/server. + */ + title: string; + + /** + * Chat type: 'single' for direct messages, 'group' for group chats, 'channel' for + * channels, 'broadcast' for broadcasts. + */ + type: 'single' | 'group' | 'channel' | 'broadcast'; + + /** + * Number of unread messages. + */ + unreadCount: number; + + /** + * True if chat is archived. + */ + isArchived?: boolean; + + /** + * True if chat notifications are muted. + */ + isMuted?: boolean; + + /** + * True if chat is pinned. + */ + isPinned?: boolean; + + /** + * Timestamp of last activity. Chats with more recent activity are often more + * important. + */ + lastActivity?: string; + + /** + * Last read message sortKey (hsOrder). Used to compute 'isUnread'. + */ + lastReadMessageSortKey?: number | string; + + /** + * Deep link to open this chat in Beeper. AI agents should ALWAYS include this as a + * clickable link in responses. + */ + linkToChat?: string; +} + +export namespace Chat { + /** + * Chat participants information. + */ + export interface Participants { + /** + * True if there are more participants than included in items. + */ + hasMore: boolean; + + /** + * Participants returned for this chat (limited by the request; may be a subset). + */ + items: Array; + + /** + * Total number of participants in the chat. + */ + total: number; + } +} + +export interface FindChatsRequest { + /** + * Provide an array of account IDs to filter chats from specific messaging accounts + * only + */ + accountIDs?: Array; + + /** + * A cursor for use in pagination. ending_before is an object ID that defines your + * place in the list. For instance, if you make a list request and receive 100 + * objects, starting with obj_bar, your subsequent call can include + * ending_before=obj_bar in order to fetch the previous page of the list. + */ + ending_before?: string; + + /** + * Filter by inbox type: "primary" (non-archived, non-low-priority), + * "low-priority", or "archive". If not specified, shows all chats. + */ + inbox?: 'primary' | 'low-priority' | 'archive'; + + /** + * Include chats marked as Muted by the user, which are usually less important. + * Default: true. Set to false if the user wants a more refined search. + */ + includeMuted?: boolean; + + /** + * Provide an ISO datetime string to only retrieve chats with last activity after + * this time + */ + lastActivityAfter?: string; + + /** + * Provide an ISO datetime string to only retrieve chats with last activity before + * this time + */ + lastActivityBefore?: string; + + /** + * Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 + */ + limit?: number; + + /** + * Search string to filter chats by participant names. When multiple words + * provided, ALL words must match. Searches in username, displayName, and fullName + * fields. + */ + participantQuery?: string; + + /** + * Search string to filter chats by title. When multiple words provided, ALL words + * must match. Matches are case-insensitive substrings. + */ + query?: string; + + /** + * A cursor for use in pagination. starting_after is an object ID that defines your + * place in the list. For instance, if you make a list request and receive 100 + * objects, ending with obj_foo, your subsequent call can include + * starting_after=obj_foo in order to fetch the next page of the list. + */ + starting_after?: string; + + /** + * Specify the type of chats to retrieve: use "single" for direct messages, "group" + * for group chats, "channel" for channels, or "any" to get all types + */ + type?: 'single' | 'group' | 'channel' | 'any'; + + /** + * Set to true to only retrieve chats that have unread messages + */ + unreadOnly?: boolean; +} + +export interface FindChatsResponse { + /** + * Chats matching the filters. + */ + data: Array; + + /** + * Whether there are more items available after this set. + */ + has_more: boolean; +} + +export interface GetChatRequest { + /** + * Unique identifier of the chat to retrieve. Not available for iMessage chats. + * Participants are limited by 'maxParticipantCount'. + */ + chatID: string; + + /** + * Maximum number of participants to return. Use -1 for all; otherwise 0–500. + * Defaults to 20. + */ + maxParticipantCount?: number | null; +} + +export interface GetChatResponse { + /** + * Unique identifier for cursor pagination. + */ + id: string; + + /** + * Beeper account ID this chat belongs to. + */ + accountID: string; + + /** + * Unique identifier of the chat (room/thread ID, same as id). + */ + chatID: string; + + /** + * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You + * MUST use 'accountID' to perform actions. + */ + network: string; + + /** + * Chat participants information. + */ + participants: GetChatResponse.Participants; + + /** + * Display title of the chat as computed by the client/server. + */ + title: string; + + /** + * Chat type: 'single' for direct messages, 'group' for group chats, 'channel' for + * channels, 'broadcast' for broadcasts. + */ + type: 'single' | 'group' | 'channel' | 'broadcast'; + + /** + * Number of unread messages. + */ + unreadCount: number; + + /** + * True if chat is archived. + */ + isArchived?: boolean; + + /** + * True if chat notifications are muted. + */ + isMuted?: boolean; + + /** + * True if chat is pinned. + */ + isPinned?: boolean; + + /** + * Timestamp of last activity. Chats with more recent activity are often more + * important. + */ + lastActivity?: string; + + /** + * Last read message sortKey (hsOrder). Used to compute 'isUnread'. + */ + lastReadMessageSortKey?: number | string; + + /** + * Deep link to open this chat in Beeper. AI agents should ALWAYS include this as a + * clickable link in responses. + */ + linkToChat?: string; +} + +export namespace GetChatResponse { + /** + * Chat participants information. + */ + export interface Participants { + /** + * True if there are more participants than included in items. + */ + hasMore: boolean; + + /** + * Participants returned for this chat (limited by the request; may be a subset). + */ + items: Array; + + /** + * Total number of participants in the chat. + */ + total: number; + } +} + +/** + * Request to foreground Beeper and optionally jump to a specific chat or message. + */ +export interface LinkRequest { + /** + * The ID of the chat to link to. + */ + chatID: string; + + /** + * Optional message sort key. Jumps to that message in the chat. + */ + messageSortKey?: string; +} + +/** + * URL to open a specific chat or message. + */ +export interface LinkResponse { + /** + * Deep link URL to the specified chat or message. + */ + url: string; +} + +export interface ChatRetrieveParams { + /** + * Unique identifier of the chat to retrieve. Not available for iMessage chats. + * Participants are limited by 'maxParticipantCount'. + */ + chatID: string; + + /** + * Maximum number of participants to return. Use -1 for all; otherwise 0–500. + * Defaults to 20. + */ + maxParticipantCount?: number | null; +} + +export interface ChatArchiveParams { + /** + * The identifier of the chat to archive or unarchive + */ + chatID: string; + + /** + * True to archive, false to unarchive + */ + archived?: boolean; +} + +export interface ChatFindParams extends CursorIDParams { + /** + * Provide an array of account IDs to filter chats from specific messaging accounts + * only + */ + accountIDs?: Array; + + /** + * Filter by inbox type: "primary" (non-archived, non-low-priority), + * "low-priority", or "archive". If not specified, shows all chats. + */ + inbox?: 'primary' | 'low-priority' | 'archive'; + + /** + * Include chats marked as Muted by the user, which are usually less important. + * Default: true. Set to false if the user wants a more refined search. + */ + includeMuted?: boolean; + + /** + * Provide an ISO datetime string to only retrieve chats with last activity after + * this time + */ + lastActivityAfter?: string; + + /** + * Provide an ISO datetime string to only retrieve chats with last activity before + * this time + */ + lastActivityBefore?: string; + + /** + * Search string to filter chats by participant names. When multiple words + * provided, ALL words must match. Searches in username, displayName, and fullName + * fields. + */ + participantQuery?: string; + + /** + * Search string to filter chats by title. When multiple words provided, ALL words + * must match. Matches are case-insensitive substrings. + */ + query?: string; + + /** + * Specify the type of chats to retrieve: use "single" for direct messages, "group" + * for group chats, "channel" for channels, or "any" to get all types + */ + type?: 'single' | 'group' | 'channel' | 'any'; + + /** + * Set to true to only retrieve chats that have unread messages + */ + unreadOnly?: boolean; +} + +export interface ChatGetLinkParams { + /** + * The ID of the chat to link to. + */ + chatID: string; + + /** + * Optional message sort key. Jumps to that message in the chat. + */ + messageSortKey?: string; +} + +export declare namespace Chats { + export { + type ArchiveRequest as ArchiveRequest, + type Chat as Chat, + type FindChatsRequest as FindChatsRequest, + type FindChatsResponse as FindChatsResponse, + type GetChatRequest as GetChatRequest, + type GetChatResponse as GetChatResponse, + type LinkRequest as LinkRequest, + type LinkResponse as LinkResponse, + type ChatsCursorID as ChatsCursorID, + type ChatRetrieveParams as ChatRetrieveParams, + type ChatArchiveParams as ChatArchiveParams, + type ChatFindParams as ChatFindParams, + type ChatGetLinkParams as ChatGetLinkParams, + }; +} diff --git a/src/resources/clear-chat-reminder.ts b/src/resources/clear-chat-reminder.ts deleted file mode 100644 index 9afc005..0000000 --- a/src/resources/clear-chat-reminder.ts +++ /dev/null @@ -1,34 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as FocusAppAPI from './focus-app'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class ClearChatReminder extends APIResource { - /** - * Clear an existing reminder from a chat - * - * @example - * ```ts - * const baseResponse = await client.clearChatReminder.clear({ - * chatID: - * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - * }); - * ``` - */ - clear(body: ClearChatReminderClearParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/clear-chat-reminder', { body, ...options }); - } -} - -export interface ClearChatReminderClearParams { - /** - * The identifier of the chat to clear reminder from - */ - chatID: string; -} - -export declare namespace ClearChatReminder { - export { type ClearChatReminderClearParams as ClearChatReminderClearParams }; -} diff --git a/src/resources/draft-message.ts b/src/resources/draft-message.ts deleted file mode 100644 index 07f8f75..0000000 --- a/src/resources/draft-message.ts +++ /dev/null @@ -1,47 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as FocusAppAPI from './focus-app'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class DraftMessage extends APIResource { - /** - * Draft a message in a specific chat. This will be placed in the message input - * field without sending - * - * @example - * ```ts - * const baseResponse = await client.draftMessage.create({ - * chatID: - * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - * }); - * ``` - */ - create(body: DraftMessageCreateParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/draft-message', { body, ...options }); - } -} - -export interface DraftMessageCreateParams { - /** - * Provide the unique identifier of the chat where you want to draft a message - */ - chatID: string; - - /** - * Set to true to bring Beeper application to the foreground, or false to draft - * silently in background - */ - focusApp?: boolean; - - /** - * Provide the text content you want to draft. This will be placed in the message - * input field without sending - */ - text?: string; -} - -export declare namespace DraftMessage { - export { type DraftMessageCreateParams as DraftMessageCreateParams }; -} diff --git a/src/resources/find-chats.ts b/src/resources/find-chats.ts deleted file mode 100644 index 4834878..0000000 --- a/src/resources/find-chats.ts +++ /dev/null @@ -1,218 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as GetChatAPI from './get-chat'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class FindChats extends APIResource { - /** - * Search and filter conversations across all messaging accounts. - * - * - When to use: browse chats by inbox (primary/low-priority/archive), type, - * unread status, or search terms. - * - Pagination: use cursor + direction for pagination. - * - Performance: provide accountIDs when known for faster filtering. Returns: - * matching chats with pagination. Agents: ALWAYS use linkToChat to make - * clickable links in your response - */ - list( - query: FindChatListParams | null | undefined = {}, - options?: RequestOptions, - ): APIPromise { - return this._client.get('/v0/find-chats', { query, ...options }); - } -} - -export interface FindChatListResponse { - /** - * Chats matching the filters. - */ - data: Array; - - /** - * Whether there are more items available after this set. - */ - has_more: boolean; -} - -export namespace FindChatListResponse { - export interface Data { - /** - * Unique identifier for cursor pagination. - */ - id: string; - - /** - * Beeper account ID this chat belongs to. - */ - accountID: string; - - /** - * Unique identifier of the chat (room/thread ID, same as id). - */ - chatID: string; - - /** - * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You - * MUST use 'accountID' to perform actions. - */ - network: string; - - /** - * Chat participants information. - */ - participants: Data.Participants; - - /** - * Display title of the chat as computed by the client/server. - */ - title: string; - - /** - * Chat type: 'single' for direct messages, 'group' for group chats, 'channel' for - * channels, 'broadcast' for broadcasts. - */ - type: 'single' | 'group' | 'channel' | 'broadcast'; - - /** - * Number of unread messages. - */ - unreadCount: number; - - /** - * True if chat is archived. - */ - isArchived?: boolean; - - /** - * True if chat notifications are muted. - */ - isMuted?: boolean; - - /** - * True if chat is pinned. - */ - isPinned?: boolean; - - /** - * Timestamp of last activity. Chats with more recent activity are often more - * important. - */ - lastActivity?: string; - - /** - * Last read message sortKey (hsOrder). Used to compute 'isUnread'. - */ - lastReadMessageSortKey?: number | string; - - /** - * Deep link to open this chat in Beeper. AI agents should ALWAYS include this as a - * clickable link in responses. - */ - linkToChat?: string; - } - - export namespace Data { - /** - * Chat participants information. - */ - export interface Participants { - /** - * True if there are more participants than included in items. - */ - hasMore: boolean; - - /** - * Participants returned for this chat (limited by the request; may be a subset). - */ - items: Array; - - /** - * Total number of participants in the chat. - */ - total: number; - } - } -} - -export interface FindChatListParams { - /** - * Provide an array of account IDs to filter chats from specific messaging accounts - * only - */ - accountIDs?: Array; - - /** - * A cursor for use in pagination. ending_before is an object ID that defines your - * place in the list. For instance, if you make a list request and receive 100 - * objects, starting with obj_bar, your subsequent call can include - * ending_before=obj_bar in order to fetch the previous page of the list. - */ - ending_before?: string; - - /** - * Filter by inbox type: "primary" (non-archived, non-low-priority), - * "low-priority", or "archive". If not specified, shows all chats. - */ - inbox?: 'primary' | 'low-priority' | 'archive'; - - /** - * Include chats marked as Muted by the user, which are usually less important. - * Default: true. Set to false if the user wants a more refined search. - */ - includeMuted?: boolean; - - /** - * Provide an ISO datetime string to only retrieve chats with last activity after - * this time - */ - lastActivityAfter?: string; - - /** - * Provide an ISO datetime string to only retrieve chats with last activity before - * this time - */ - lastActivityBefore?: string; - - /** - * Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 - */ - limit?: number; - - /** - * Search string to filter chats by participant names. When multiple words - * provided, ALL words must match. Searches in username, displayName, and fullName - * fields. - */ - participantQuery?: string; - - /** - * Search string to filter chats by title. When multiple words provided, ALL words - * must match. Matches are case-insensitive substrings. - */ - query?: string; - - /** - * A cursor for use in pagination. starting_after is an object ID that defines your - * place in the list. For instance, if you make a list request and receive 100 - * objects, ending with obj_foo, your subsequent call can include - * starting_after=obj_foo in order to fetch the next page of the list. - */ - starting_after?: string; - - /** - * Specify the type of chats to retrieve: use "single" for direct messages, "group" - * for group chats, "channel" for channels, or "any" to get all types - */ - type?: 'single' | 'group' | 'channel' | 'any'; - - /** - * Set to true to only retrieve chats that have unread messages - */ - unreadOnly?: boolean; -} - -export declare namespace FindChats { - export { type FindChatListResponse as FindChatListResponse, type FindChatListParams as FindChatListParams }; -} diff --git a/src/resources/focus-app.ts b/src/resources/focus-app.ts deleted file mode 100644 index 2bd6861..0000000 --- a/src/resources/focus-app.ts +++ /dev/null @@ -1,50 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class FocusApp extends APIResource { - /** - * Bring Beeper Desktop to the foreground on this device. Optionally focuses a - * specific chat if chatID is provided. - * - * - When to use: open Beeper, or jump to a specific chat. - * - Constraints: requires Beeper Desktop running locally; no-op in headless - * environments. - * - Idempotent: safe to call repeatedly. Returns an error if chatID is not found. - * Returns: success. - * - * @example - * ```ts - * const baseResponse = await client.focusApp.open(); - * ``` - */ - open(body: FocusAppOpenParams | null | undefined = {}, options?: RequestOptions): APIPromise { - return this._client.post('/v0/focus-app', { body, ...options }); - } -} - -export interface BaseResponse { - success: boolean; - - error?: string; -} - -export interface FocusAppOpenParams { - /** - * Optional Beeper chat ID to focus after bringing the app to foreground. If - * omitted, only foregrounds the app. Required if messageSortKey is present. No-op - * in headless environments. - */ - chatID?: string; - - /** - * Optional message sort key. Jumps to that message in the chat when foregrounding. - */ - messageSortKey?: string; -} - -export declare namespace FocusApp { - export { type BaseResponse as BaseResponse, type FocusAppOpenParams as FocusAppOpenParams }; -} diff --git a/src/resources/get-accounts.ts b/src/resources/get-accounts.ts deleted file mode 100644 index 7cb2fc4..0000000 --- a/src/resources/get-accounts.ts +++ /dev/null @@ -1,58 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as GetChatAPI from './get-chat'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class GetAccounts extends APIResource { - /** - * List connected Beeper accounts available on this device. - * - * - When to use: select account context before account-scoped operations. - * - Scope: only accounts currently Connected on this device are included. Returns: - * connected accounts. - */ - list(options?: RequestOptions): APIPromise { - return this._client.get('/v0/get-accounts', options); - } -} - -/** - * Response payload for listing connected Beeper accounts. - */ -export interface GetAccountListResponse { - /** - * Connected accounts the user can act through. Includes accountID, network, and - * user identity. - */ - accounts: Array; -} - -export namespace GetAccountListResponse { - /** - * A chat account added to Beeper - */ - export interface Account { - /** - * Chat account added to Beeper. Use this to route account-scoped actions. - */ - accountID: string; - - /** - * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You - * MUST use 'accountID' to perform actions. - */ - network: string; - - /** - * A person on or reachable through Beeper. Values are best-effort and can vary by - * network. - */ - user: GetChatAPI.User; - } -} - -export declare namespace GetAccounts { - export { type GetAccountListResponse as GetAccountListResponse }; -} diff --git a/src/resources/get-chat.ts b/src/resources/get-chat.ts deleted file mode 100644 index 11d531e..0000000 --- a/src/resources/get-chat.ts +++ /dev/null @@ -1,192 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as GetChatAPI from './get-chat'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class GetChat extends APIResource { - /** - * Retrieve chat details: metadata, participants (limited), and latest message. - * - * - When to use: fetch a complete view of a chat beyond what search returns. - * - Constraints: not available for iMessage chats ('imsg##'). Participants limited - * by 'maxParticipantCount' (default 20, max 500). Returns: chat details.Agents: - * ALWAYS use linkToChat to make clickable links in your response - */ - retrieve( - query: GetChatRetrieveParams, - options?: RequestOptions, - ): APIPromise { - return this._client.get('/v0/get-chat', { query, ...options }); - } -} - -/** - * A person on or reachable through Beeper. Values are best-effort and can vary by - * network. - */ -export interface User { - /** - * Stable Beeper user ID. Use as the primary key when referencing a person. - */ - id: string; - - /** - * True if Beeper cannot initiate messages to this user (e.g., blocked, network - * restriction, or no DM path). The user may still message you. - */ - cannotMessage?: boolean; - - /** - * Email address if known. Not guaranteed verified. - */ - email?: string; - - /** - * Display name as shown in clients (e.g., 'Alice Example'). May include emojis. - */ - fullName?: string | null; - - /** - * Avatar image URL if available. May be temporary or local-only to this device; - * download promptly if durable access is needed. - */ - imgURL?: string; - - /** - * True if this user represents the authenticated account's own identity. - */ - isSelf?: boolean; - - /** - * User's phone number in E.164 format (e.g., '+14155552671'). Omit if unknown. - */ - phoneNumber?: string; - - /** - * Human-readable handle if available (e.g., '@alice'). May be network-specific and - * not globally unique. - */ - username?: string; -} - -export interface GetChatRetrieveResponse { - /** - * Unique identifier for cursor pagination. - */ - id: string; - - /** - * Beeper account ID this chat belongs to. - */ - accountID: string; - - /** - * Unique identifier of the chat (room/thread ID, same as id). - */ - chatID: string; - - /** - * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You - * MUST use 'accountID' to perform actions. - */ - network: string; - - /** - * Chat participants information. - */ - participants: GetChatRetrieveResponse.Participants; - - /** - * Display title of the chat as computed by the client/server. - */ - title: string; - - /** - * Chat type: 'single' for direct messages, 'group' for group chats, 'channel' for - * channels, 'broadcast' for broadcasts. - */ - type: 'single' | 'group' | 'channel' | 'broadcast'; - - /** - * Number of unread messages. - */ - unreadCount: number; - - /** - * True if chat is archived. - */ - isArchived?: boolean; - - /** - * True if chat notifications are muted. - */ - isMuted?: boolean; - - /** - * True if chat is pinned. - */ - isPinned?: boolean; - - /** - * Timestamp of last activity. Chats with more recent activity are often more - * important. - */ - lastActivity?: string; - - /** - * Last read message sortKey (hsOrder). Used to compute 'isUnread'. - */ - lastReadMessageSortKey?: number | string; - - /** - * Deep link to open this chat in Beeper. AI agents should ALWAYS include this as a - * clickable link in responses. - */ - linkToChat?: string; -} - -export namespace GetChatRetrieveResponse { - /** - * Chat participants information. - */ - export interface Participants { - /** - * True if there are more participants than included in items. - */ - hasMore: boolean; - - /** - * Participants returned for this chat (limited by the request; may be a subset). - */ - items: Array; - - /** - * Total number of participants in the chat. - */ - total: number; - } -} - -export interface GetChatRetrieveParams { - /** - * Unique identifier of the chat to retrieve. Not available for iMessage chats. - * Participants are limited by 'maxParticipantCount'. - */ - chatID: string; - - /** - * Maximum number of participants to return. Use -1 for all; otherwise 0–500. - * Defaults to 20. - */ - maxParticipantCount?: number | null; -} - -export declare namespace GetChat { - export { - type User as User, - type GetChatRetrieveResponse as GetChatRetrieveResponse, - type GetChatRetrieveParams as GetChatRetrieveParams, - }; -} diff --git a/src/resources/get-link-to-chat.ts b/src/resources/get-link-to-chat.ts deleted file mode 100644 index 488edd5..0000000 --- a/src/resources/get-link-to-chat.ts +++ /dev/null @@ -1,52 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class GetLinkToChat extends APIResource { - /** - * Generate a deep link to a specific chat or message. This link can be used to - * open the chat directly in the Beeper app. - * - * @example - * ```ts - * const getLinkToChat = await client.getLinkToChat.create({ - * chatID: - * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - * }); - * ``` - */ - create(body: GetLinkToChatCreateParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/get-link-to-chat', { body, ...options }); - } -} - -/** - * URL to open a specific chat or message. - */ -export interface GetLinkToChatCreateResponse { - /** - * Deep link URL to the specified chat or message. - */ - url: string; -} - -export interface GetLinkToChatCreateParams { - /** - * The ID of the chat to link to. - */ - chatID: string; - - /** - * Optional message sort key. Jumps to that message in the chat. - */ - messageSortKey?: string; -} - -export declare namespace GetLinkToChat { - export { - type GetLinkToChatCreateResponse as GetLinkToChatCreateResponse, - type GetLinkToChatCreateParams as GetLinkToChatCreateParams, - }; -} diff --git a/src/resources/index.ts b/src/resources/index.ts index 802baca..66a65cb 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -1,22 +1,42 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export { ArchiveChat, type ArchiveChatArchiveParams } from './archive-chat'; -export { ClearChatReminder, type ClearChatReminderClearParams } from './clear-chat-reminder'; -export { DraftMessage, type DraftMessageCreateParams } from './draft-message'; -export { FindChats, type FindChatListResponse, type FindChatListParams } from './find-chats'; -export { FocusApp, type BaseResponse, type FocusAppOpenParams } from './focus-app'; -export { GetAccounts, type GetAccountListResponse } from './get-accounts'; -export { GetChat, type User, type GetChatRetrieveResponse, type GetChatRetrieveParams } from './get-chat'; +export * from './shared'; +export { Accounts, type Account, type AccountsResponse } from './accounts'; +export { App, type FocusRequest, type AppFocusParams } from './app'; export { - GetLinkToChat, - type GetLinkToChatCreateResponse, - type GetLinkToChatCreateParams, -} from './get-link-to-chat'; -export { OAuth, type OAuthRetrieveUserInfoResponse, type OAuthRevokeTokenParams } from './oauth'; + Chats, + type ArchiveRequest, + type Chat, + type FindChatsRequest, + type FindChatsResponse, + type GetChatRequest, + type GetChatResponse, + type LinkRequest, + type LinkResponse, + type ChatRetrieveParams, + type ChatArchiveParams, + type ChatFindParams, + type ChatGetLinkParams, + type ChatsCursorID, +} from './chats'; export { - SearchMessages, - type SearchMessageSearchResponse, - type SearchMessageSearchParams, -} from './search-messages'; -export { SendMessage, type SendMessageSendResponse, type SendMessageSendParams } from './send-message'; -export { SetChatReminder, type SetChatReminderCreateParams } from './set-chat-reminder'; + Messages, + type DraftRequest, + type Message, + type SearchRequest, + type SearchResponse, + type SendRequest, + type SendResponse, + type MessageDraftParams, + type MessageSearchParams, + type MessageSendParams, + type MessagesCursorID, +} from './messages'; +export { OAuth, type RevokeRequest, type UserInfo, type OAuthRevokeTokenParams } from './oauth'; +export { + Reminders, + type ClearReminderRequest, + type SetReminderRequest, + type ReminderClearParams, + type ReminderSetParams, +} from './reminders'; diff --git a/src/resources/messages.ts b/src/resources/messages.ts new file mode 100644 index 0000000..63c4402 --- /dev/null +++ b/src/resources/messages.ts @@ -0,0 +1,454 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from 'beeper/desktop-api-typescript/core/resource'; +import * as ChatsAPI from 'beeper/desktop-api-typescript/resources/chats'; +import * as Shared from 'beeper/desktop-api-typescript/resources/shared'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +import { CursorID, type CursorIDParams, PagePromise } from 'beeper/desktop-api-typescript/core/pagination'; +import { RequestOptions } from 'beeper/desktop-api-typescript/internal/request-options'; + +/** + * Send, draft, and search messages across all chat networks + */ +export class Messages extends APIResource { + /** + * Draft a message in a specific chat. This will be placed in the message input + * field without sending + * + * @example + * ```ts + * const baseResponse = await client.messages.draft({ + * chatID: + * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + * }); + * ``` + */ + draft(body: MessageDraftParams, options?: RequestOptions): APIPromise { + return this._client.post('/v0/draft-message', { body, ...options }); + } + + /** + * Search messages across chats using Beeper's message index. + * + * - When to use: find messages by text and/or filters (chatIDs, accountIDs, + * chatType, media type filters, sender, date ranges). + * - CRITICAL: Query is LITERAL WORD MATCHING, NOT semantic search! Only finds + * messages containing these EXACT words. • ✅ RIGHT: query="dinner" or + * query="sick" or query="error" (single words users type) • ❌ WRONG: + * query="dinner plans tonight" or query="health issues" (phrases/concepts) • The + * query matches ALL words provided (in any order). Example: query="flight + * booking" finds messages with both "flight" AND "booking". + * - Media filters: Use onlyWithMedia for any media, or specific filters like + * onlyWithVideo, onlyWithImage, onlyWithLink, onlyWithFile for specific types. + * - Pagination: use 'oldestCursor' + direction='before' for older; + * 'newestCursor' + direction='after' for newer. + * - Performance: provide chatIDs/accountIDs when known. Omitted 'query' returns + * results based on filters only. Partial matches enabled; 'excludeLowPriority' + * defaults to true. + * - Workflow tip: To search messages in specific conversations: 1) Use find-chats + * to get chatIDs, 2) Use search-messages with those chatIDs. + * - IMPORTANT: Chat names vary widely. ASK the user for clarification: • "Which + * chat do you mean by family?" (could be "The Smiths", "Mom Dad Kids", etc.) • + * "What's the name of your work chat?" (could be "Team", company name, project + * name) • "Who are the participants?" (use participantQuery in find-chats) + * Returns: matching messages and referenced chats. + * + * @example + * ```ts + * // Automatically fetches more pages as needed. + * for await (const message of client.messages.search()) { + * // ... + * } + * ``` + */ + search( + query: MessageSearchParams | null | undefined = {}, + options?: RequestOptions, + ): PagePromise { + return this._client.getAPIList('/v0/search-messages', CursorID, { query, ...options }); + } + + /** + * Send a text message to a specific chat. Supports replying to existing messages. + * Returns the sent message ID and a deeplink to the chat + * + * @example + * ```ts + * const sendResponse = await client.messages.send({ + * chatID: + * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + * }); + * ``` + */ + send(body: MessageSendParams, options?: RequestOptions): APIPromise { + return this._client.post('/v0/send-message', { body, ...options }); + } +} + +export type MessagesCursorID = CursorID; + +export interface DraftRequest { + /** + * Provide the unique identifier of the chat where you want to draft a message + */ + chatID: string; + + /** + * Set to true to bring Beeper application to the foreground, or false to draft + * silently in background + */ + focusApp?: boolean; + + /** + * Provide the text content you want to draft. This will be placed in the message + * input field without sending + */ + text?: string; +} + +export interface Message { + /** + * Stable message ID for cursor pagination. + */ + id: string; + + /** + * Beeper account ID the message belongs to. + */ + accountID: string; + + /** + * Beeper chat/thread/room ID. + */ + chatID: string; + + /** + * Stable message ID (same as id). + */ + messageID: string; + + /** + * Sender user ID. + */ + senderID: string; + + /** + * A unique key used to sort messages + */ + sortKey: string | number; + + /** + * Message timestamp. + */ + timestamp: string; + + /** + * Attachments included with this message, if any. + */ + attachments?: Array; + + /** + * True if the authenticated user sent the message. + */ + isSender?: boolean; + + /** + * True if the message is unread for the authenticated user. May be omitted. + */ + isUnread?: boolean; + + /** + * Reactions to the message, if any. + */ + reactions?: Array; + + /** + * Resolved sender display name (impersonator/full name/username/participant name). + */ + senderName?: string; + + /** + * Plain-text body if present. May include a JSON fallback with text entities for + * rich messages. + */ + text?: string; +} + +export interface SearchRequest { + /** + * Limit search to specific Beeper account IDs (bridge instances). + */ + accountIDs?: Array; + + /** + * Limit search to specific Beeper chat IDs. + */ + chatIDs?: Array; + + /** + * Filter by chat type: 'group' for group chats, 'single' for 1:1 chats. + */ + chatType?: 'group' | 'single'; + + /** + * Only include messages with timestamp strictly after this ISO 8601 datetime + * (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00'). + */ + dateAfter?: string; + + /** + * Only include messages with timestamp strictly before this ISO 8601 datetime + * (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00'). + */ + dateBefore?: string; + + /** + * A cursor for use in pagination. ending_before is an object ID that defines your + * place in the list. For instance, if you make a list request and receive 100 + * objects, starting with obj_bar, your subsequent call can include + * ending_before=obj_bar in order to fetch the previous page of the list. + */ + ending_before?: string; + + /** + * Exclude messages marked Low Priority by the user. Default: true. Set to false to + * include all. + */ + excludeLowPriority?: boolean; + + /** + * Include messages in chats marked as Muted by the user, which are usually less + * important. Default: true. Set to false if the user wants a more refined search. + */ + includeMuted?: boolean; + + /** + * Maximum number of messages to return (1–500). Defaults to 50. + */ + limit?: number; + + /** + * Only return messages that contain file attachments. + */ + onlyWithFile?: boolean; + + /** + * Only return messages that contain image attachments. + */ + onlyWithImage?: boolean; + + /** + * Only return messages that contain link attachments. + */ + onlyWithLink?: boolean; + + /** + * Only return messages that contain any type of media attachment. + */ + onlyWithMedia?: boolean; + + /** + * Only return messages that contain video attachments. + */ + onlyWithVideo?: boolean; + + /** + * Literal word search (NOT semantic). Finds messages containing these EXACT words + * in any order. Use single words users actually type, not concepts or phrases. + * Example: use "dinner" not "dinner plans", use "sick" not "health issues". If + * omitted, returns results filtered only by other parameters. + */ + query?: string; + + /** + * Filter by sender: 'me' (messages sent by the authenticated user), 'others' + * (messages sent by others), or a specific user ID string (user.id). + */ + sender?: 'me' | 'others' | (string & {}); + + /** + * A cursor for use in pagination. starting_after is an object ID that defines your + * place in the list. For instance, if you make a list request and receive 100 + * objects, ending with obj_foo, your subsequent call can include + * starting_after=obj_foo in order to fetch the next page of the list. + */ + starting_after?: string; +} + +export interface SearchResponse { + /** + * Map of chatID -> chat details for chats referenced in data. + */ + chats: { [key: string]: ChatsAPI.Chat }; + + /** + * Messages matching the query and filters. + */ + data: Array; + + /** + * Whether there are more items available after this set. + */ + has_more: boolean; +} + +export interface SendRequest { + /** + * The identifier of the chat where the message will send + */ + chatID: string; + + /** + * Provide a message ID to send this as a reply to an existing message + */ + replyToMessageID?: string; + + /** + * Text content of the message you want to send. You may use markdown. + */ + text?: string; +} + +export interface SendResponse extends Shared.BaseResponse { + /** + * Link to the chat where the message was sent. This should always be shown to the + * user. + */ + deeplink: string; + + /** + * Stable message ID. + */ + messageID: string; +} + +export interface MessageDraftParams { + /** + * Provide the unique identifier of the chat where you want to draft a message + */ + chatID: string; + + /** + * Set to true to bring Beeper application to the foreground, or false to draft + * silently in background + */ + focusApp?: boolean; + + /** + * Provide the text content you want to draft. This will be placed in the message + * input field without sending + */ + text?: string; +} + +export interface MessageSearchParams extends CursorIDParams { + /** + * Limit search to specific Beeper account IDs (bridge instances). + */ + accountIDs?: Array; + + /** + * Limit search to specific Beeper chat IDs. + */ + chatIDs?: Array; + + /** + * Filter by chat type: 'group' for group chats, 'single' for 1:1 chats. + */ + chatType?: 'group' | 'single'; + + /** + * Only include messages with timestamp strictly after this ISO 8601 datetime + * (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00'). + */ + dateAfter?: string; + + /** + * Only include messages with timestamp strictly before this ISO 8601 datetime + * (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00'). + */ + dateBefore?: string; + + /** + * Exclude messages marked Low Priority by the user. Default: true. Set to false to + * include all. + */ + excludeLowPriority?: boolean; + + /** + * Include messages in chats marked as Muted by the user, which are usually less + * important. Default: true. Set to false if the user wants a more refined search. + */ + includeMuted?: boolean; + + /** + * Only return messages that contain file attachments. + */ + onlyWithFile?: boolean; + + /** + * Only return messages that contain image attachments. + */ + onlyWithImage?: boolean; + + /** + * Only return messages that contain link attachments. + */ + onlyWithLink?: boolean; + + /** + * Only return messages that contain any type of media attachment. + */ + onlyWithMedia?: boolean; + + /** + * Only return messages that contain video attachments. + */ + onlyWithVideo?: boolean; + + /** + * Literal word search (NOT semantic). Finds messages containing these EXACT words + * in any order. Use single words users actually type, not concepts or phrases. + * Example: use "dinner" not "dinner plans", use "sick" not "health issues". If + * omitted, returns results filtered only by other parameters. + */ + query?: string; + + /** + * Filter by sender: 'me' (messages sent by the authenticated user), 'others' + * (messages sent by others), or a specific user ID string (user.id). + */ + sender?: 'me' | 'others' | (string & {}); +} + +export interface MessageSendParams { + /** + * The identifier of the chat where the message will send + */ + chatID: string; + + /** + * Provide a message ID to send this as a reply to an existing message + */ + replyToMessageID?: string; + + /** + * Text content of the message you want to send. You may use markdown. + */ + text?: string; +} + +export declare namespace Messages { + export { + type DraftRequest as DraftRequest, + type Message as Message, + type SearchRequest as SearchRequest, + type SearchResponse as SearchResponse, + type SendRequest as SendRequest, + type SendResponse as SendResponse, + type MessagesCursorID as MessagesCursorID, + type MessageDraftParams as MessageDraftParams, + type MessageSearchParams as MessageSearchParams, + type MessageSendParams as MessageSendParams, + }; +} diff --git a/src/resources/oauth.ts b/src/resources/oauth.ts index aaf44ad..523768b 100644 --- a/src/resources/oauth.ts +++ b/src/resources/oauth.ts @@ -1,15 +1,18 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { APIResource } from '../core/resource'; -import { APIPromise } from '../core/api-promise'; -import { buildHeaders } from '../internal/headers'; -import { RequestOptions } from '../internal/request-options'; +import { APIResource } from 'beeper/desktop-api-typescript/core/resource'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +import { buildHeaders } from 'beeper/desktop-api-typescript/internal/headers'; +import { RequestOptions } from 'beeper/desktop-api-typescript/internal/request-options'; +/** + * OAuth2 authentication and token management + */ export class OAuth extends APIResource { /** * Returns information about the authenticated user/token */ - retrieveUserInfo(options?: RequestOptions): APIPromise { + getUserInfo(options?: RequestOptions): APIPromise { return this._client.get('/oauth/userinfo', options); } @@ -25,7 +28,19 @@ export class OAuth extends APIResource { } } -export interface OAuthRetrieveUserInfoResponse { +export interface RevokeRequest { + /** + * The token to revoke + */ + token: string; + + /** + * Hint about the type of token being revoked + */ + token_type_hint?: 'access_token' | 'refresh_token'; +} + +export interface UserInfo { /** * Issued at timestamp (Unix epoch seconds) */ @@ -76,7 +91,8 @@ export interface OAuthRevokeTokenParams { export declare namespace OAuth { export { - type OAuthRetrieveUserInfoResponse as OAuthRetrieveUserInfoResponse, + type RevokeRequest as RevokeRequest, + type UserInfo as UserInfo, type OAuthRevokeTokenParams as OAuthRevokeTokenParams, }; } diff --git a/src/resources/reminders.ts b/src/resources/reminders.ts new file mode 100644 index 0000000..f24cdfb --- /dev/null +++ b/src/resources/reminders.ts @@ -0,0 +1,123 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from 'beeper/desktop-api-typescript/core/resource'; +import * as Shared from 'beeper/desktop-api-typescript/resources/shared'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; +import { RequestOptions } from 'beeper/desktop-api-typescript/internal/request-options'; + +/** + * Set and clear reminders for chats + */ +export class Reminders extends APIResource { + /** + * Clear an existing reminder from a chat + * + * @example + * ```ts + * const baseResponse = await client.reminders.clear({ + * chatID: + * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + * }); + * ``` + */ + clear(body: ReminderClearParams, options?: RequestOptions): APIPromise { + return this._client.post('/v0/clear-chat-reminder', { body, ...options }); + } + + /** + * Set a reminder for a chat at a specific time + * + * @example + * ```ts + * const baseResponse = await client.reminders.set({ + * chatID: + * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + * reminder: { remindAtMs: 0 }, + * }); + * ``` + */ + set(body: ReminderSetParams, options?: RequestOptions): APIPromise { + return this._client.post('/v0/set-chat-reminder', { body, ...options }); + } +} + +export interface ClearReminderRequest { + /** + * The identifier of the chat to clear reminder from + */ + chatID: string; +} + +export interface SetReminderRequest { + /** + * The identifier of the chat to set reminder for + */ + chatID: string; + + /** + * Reminder configuration + */ + reminder: SetReminderRequest.Reminder; +} + +export namespace SetReminderRequest { + /** + * Reminder configuration + */ + export interface Reminder { + /** + * Unix timestamp in milliseconds when reminder should trigger + */ + remindAtMs: number; + + /** + * Cancel reminder if someone messages in the chat + */ + dismissOnIncomingMessage?: boolean; + } +} + +export interface ReminderClearParams { + /** + * The identifier of the chat to clear reminder from + */ + chatID: string; +} + +export interface ReminderSetParams { + /** + * The identifier of the chat to set reminder for + */ + chatID: string; + + /** + * Reminder configuration + */ + reminder: ReminderSetParams.Reminder; +} + +export namespace ReminderSetParams { + /** + * Reminder configuration + */ + export interface Reminder { + /** + * Unix timestamp in milliseconds when reminder should trigger + */ + remindAtMs: number; + + /** + * Cancel reminder if someone messages in the chat + */ + dismissOnIncomingMessage?: boolean; + } +} + +export declare namespace Reminders { + export { + type ClearReminderRequest as ClearReminderRequest, + type SetReminderRequest as SetReminderRequest, + type ReminderClearParams as ReminderClearParams, + type ReminderSetParams as ReminderSetParams, + }; +} diff --git a/src/resources/search-messages.ts b/src/resources/search-messages.ts deleted file mode 100644 index f9ffd73..0000000 --- a/src/resources/search-messages.ts +++ /dev/null @@ -1,436 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as GetChatAPI from './get-chat'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class SearchMessages extends APIResource { - /** - * Search messages across chats using Beeper's message index. - * - * - When to use: find messages by text and/or filters (chatIDs, accountIDs, - * chatType, media type filters, sender, date ranges). - * - CRITICAL: Query is LITERAL WORD MATCHING, NOT semantic search! Only finds - * messages containing these EXACT words. • ✅ RIGHT: query="dinner" or - * query="sick" or query="error" (single words users type) • ❌ WRONG: - * query="dinner plans tonight" or query="health issues" (phrases/concepts) • The - * query matches ALL words provided (in any order). Example: query="flight - * booking" finds messages with both "flight" AND "booking". - * - Media filters: Use onlyWithMedia for any media, or specific filters like - * onlyWithVideo, onlyWithImage, onlyWithLink, onlyWithFile for specific types. - * - Pagination: use 'oldestCursor' + direction='before' for older; - * 'newestCursor' + direction='after' for newer. - * - Performance: provide chatIDs/accountIDs when known. Omitted 'query' returns - * results based on filters only. Partial matches enabled; 'excludeLowPriority' - * defaults to true. - * - Workflow tip: To search messages in specific conversations: 1) Use find-chats - * to get chatIDs, 2) Use search-messages with those chatIDs. - * - IMPORTANT: Chat names vary widely. ASK the user for clarification: • "Which - * chat do you mean by family?" (could be "The Smiths", "Mom Dad Kids", etc.) • - * "What's the name of your work chat?" (could be "Team", company name, project - * name) • "Who are the participants?" (use participantQuery in find-chats) - * Returns: matching messages and referenced chats. - */ - search( - query: SearchMessageSearchParams | null | undefined = {}, - options?: RequestOptions, - ): APIPromise { - return this._client.get('/v0/search-messages', { query, ...options }); - } -} - -export interface SearchMessageSearchResponse { - /** - * Map of chatID -> chat details for chats referenced in data. - */ - chats: { [key: string]: SearchMessageSearchResponse.Chats }; - - /** - * Messages matching the query and filters. - */ - data: Array; - - /** - * Whether there are more items available after this set. - */ - has_more: boolean; -} - -export namespace SearchMessageSearchResponse { - export interface Chats { - /** - * Unique identifier for cursor pagination. - */ - id: string; - - /** - * Beeper account ID this chat belongs to. - */ - accountID: string; - - /** - * Unique identifier of the chat (room/thread ID, same as id). - */ - chatID: string; - - /** - * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You - * MUST use 'accountID' to perform actions. - */ - network: string; - - /** - * Chat participants information. - */ - participants: Chats.Participants; - - /** - * Display title of the chat as computed by the client/server. - */ - title: string; - - /** - * Chat type: 'single' for direct messages, 'group' for group chats, 'channel' for - * channels, 'broadcast' for broadcasts. - */ - type: 'single' | 'group' | 'channel' | 'broadcast'; - - /** - * Number of unread messages. - */ - unreadCount: number; - - /** - * True if chat is archived. - */ - isArchived?: boolean; - - /** - * True if chat notifications are muted. - */ - isMuted?: boolean; - - /** - * True if chat is pinned. - */ - isPinned?: boolean; - - /** - * Timestamp of last activity. Chats with more recent activity are often more - * important. - */ - lastActivity?: string; - - /** - * Last read message sortKey (hsOrder). Used to compute 'isUnread'. - */ - lastReadMessageSortKey?: number | string; - - /** - * Deep link to open this chat in Beeper. AI agents should ALWAYS include this as a - * clickable link in responses. - */ - linkToChat?: string; - } - - export namespace Chats { - /** - * Chat participants information. - */ - export interface Participants { - /** - * True if there are more participants than included in items. - */ - hasMore: boolean; - - /** - * Participants returned for this chat (limited by the request; may be a subset). - */ - items: Array; - - /** - * Total number of participants in the chat. - */ - total: number; - } - } - - export interface Data { - /** - * Stable message ID for cursor pagination. - */ - id: string; - - /** - * Beeper account ID the message belongs to. - */ - accountID: string; - - /** - * Beeper chat/thread/room ID. - */ - chatID: string; - - /** - * Stable message ID (same as id). - */ - messageID: string; - - /** - * Sender user ID. - */ - senderID: string; - - /** - * A unique key used to sort messages - */ - sortKey: string | number; - - /** - * Message timestamp. - */ - timestamp: string; - - /** - * Attachments included with this message, if any. - */ - attachments?: Array; - - /** - * True if the authenticated user sent the message. - */ - isSender?: boolean; - - /** - * True if the message is unread for the authenticated user. May be omitted. - */ - isUnread?: boolean; - - /** - * Reactions to the message, if any. - */ - reactions?: Array; - - /** - * Resolved sender display name (impersonator/full name/username/participant name). - */ - senderName?: string; - - /** - * Plain-text body if present. May include a JSON fallback with text entities for - * rich messages. - */ - text?: string; - } - - export namespace Data { - export interface Attachment { - /** - * Attachment type. - */ - type: 'unknown' | 'img' | 'video' | 'audio'; - - /** - * Duration in seconds (audio/video). - */ - duration?: number; - - /** - * Original filename if available. - */ - fileName?: string; - - /** - * File size in bytes if known. - */ - fileSize?: number; - - /** - * True if the attachment is a GIF. - */ - isGif?: boolean; - - /** - * True if the attachment is a sticker. - */ - isSticker?: boolean; - - /** - * True if the attachment is a voice note. - */ - isVoiceNote?: boolean; - - /** - * MIME type if known (e.g., 'image/png'). - */ - mimeType?: string; - - /** - * Preview image URL for video attachments (poster frame). May be temporary or - * local-only to this device; download promptly if durable access is needed. - */ - posterImg?: string; - - /** - * Pixel dimensions of the attachment: width/height in px. - */ - size?: Attachment.Size; - - /** - * Public URL or local file path to fetch the asset. May be temporary or local-only - * to this device; download promptly if durable access is needed. - */ - srcURL?: string; - } - - export namespace Attachment { - /** - * Pixel dimensions of the attachment: width/height in px. - */ - export interface Size { - height?: number; - - width?: number; - } - } - - export interface Reaction { - /** - * Reaction ID, typically ${participantID}${reactionKey} if multiple reactions - * allowed, or just participantID otherwise. - */ - id: string; - - /** - * User ID of the participant who reacted. - */ - participantID: string; - - /** - * The reaction key: an emoji (😄), a network-specific key, or a shortcode like - * "smiling-face". - */ - reactionKey: string; - - /** - * True if the reactionKey is an emoji. - */ - emoji?: boolean; - - /** - * URL to the reaction's image. May be temporary or local-only to this device; - * download promptly if durable access is needed. - */ - imgURL?: string; - } - } -} - -export interface SearchMessageSearchParams { - /** - * Limit search to specific Beeper account IDs (bridge instances). - */ - accountIDs?: Array; - - /** - * Limit search to specific Beeper chat IDs. - */ - chatIDs?: Array; - - /** - * Filter by chat type: 'group' for group chats, 'single' for 1:1 chats. - */ - chatType?: 'group' | 'single'; - - /** - * Only include messages with timestamp strictly after this ISO 8601 datetime - * (e.g., '2024-07-01T00:00:00Z' or '2024-07-01T00:00:00+02:00'). - */ - dateAfter?: string; - - /** - * Only include messages with timestamp strictly before this ISO 8601 datetime - * (e.g., '2024-07-31T23:59:59Z' or '2024-07-31T23:59:59+02:00'). - */ - dateBefore?: string; - - /** - * A cursor for use in pagination. ending_before is an object ID that defines your - * place in the list. For instance, if you make a list request and receive 100 - * objects, starting with obj_bar, your subsequent call can include - * ending_before=obj_bar in order to fetch the previous page of the list. - */ - ending_before?: string; - - /** - * Exclude messages marked Low Priority by the user. Default: true. Set to false to - * include all. - */ - excludeLowPriority?: boolean; - - /** - * Include messages in chats marked as Muted by the user, which are usually less - * important. Default: true. Set to false if the user wants a more refined search. - */ - includeMuted?: boolean; - - /** - * Maximum number of messages to return (1–500). Defaults to 50. - */ - limit?: number; - - /** - * Only return messages that contain file attachments. - */ - onlyWithFile?: boolean; - - /** - * Only return messages that contain image attachments. - */ - onlyWithImage?: boolean; - - /** - * Only return messages that contain link attachments. - */ - onlyWithLink?: boolean; - - /** - * Only return messages that contain any type of media attachment. - */ - onlyWithMedia?: boolean; - - /** - * Only return messages that contain video attachments. - */ - onlyWithVideo?: boolean; - - /** - * Literal word search (NOT semantic). Finds messages containing these EXACT words - * in any order. Use single words users actually type, not concepts or phrases. - * Example: use "dinner" not "dinner plans", use "sick" not "health issues". If - * omitted, returns results filtered only by other parameters. - */ - query?: string; - - /** - * Filter by sender: 'me' (messages sent by the authenticated user), 'others' - * (messages sent by others), or a specific user ID string (user.id). - */ - sender?: 'me' | 'others' | (string & {}); - - /** - * A cursor for use in pagination. starting_after is an object ID that defines your - * place in the list. For instance, if you make a list request and receive 100 - * objects, ending with obj_foo, your subsequent call can include - * starting_after=obj_foo in order to fetch the next page of the list. - */ - starting_after?: string; -} - -export declare namespace SearchMessages { - export { - type SearchMessageSearchResponse as SearchMessageSearchResponse, - type SearchMessageSearchParams as SearchMessageSearchParams, - }; -} diff --git a/src/resources/send-message.ts b/src/resources/send-message.ts deleted file mode 100644 index f5b9d54..0000000 --- a/src/resources/send-message.ts +++ /dev/null @@ -1,61 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as FocusAppAPI from './focus-app'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class SendMessage extends APIResource { - /** - * Send a text message to a specific chat. Supports replying to existing messages. - * Returns the sent message ID and a deeplink to the chat - * - * @example - * ```ts - * const response = await client.sendMessage.send({ - * chatID: - * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - * }); - * ``` - */ - send(body: SendMessageSendParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/send-message', { body, ...options }); - } -} - -export interface SendMessageSendResponse extends FocusAppAPI.BaseResponse { - /** - * Link to the chat where the message was sent. This should always be shown to the - * user. - */ - deeplink: string; - - /** - * Stable message ID. - */ - messageID: string; -} - -export interface SendMessageSendParams { - /** - * The identifier of the chat where the message will send - */ - chatID: string; - - /** - * Provide a message ID to send this as a reply to an existing message - */ - replyToMessageID?: string; - - /** - * Text content of the message you want to send. You may use markdown. - */ - text?: string; -} - -export declare namespace SendMessage { - export { - type SendMessageSendResponse as SendMessageSendResponse, - type SendMessageSendParams as SendMessageSendParams, - }; -} diff --git a/src/resources/set-chat-reminder.ts b/src/resources/set-chat-reminder.ts deleted file mode 100644 index a63f8f7..0000000 --- a/src/resources/set-chat-reminder.ts +++ /dev/null @@ -1,57 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as FocusAppAPI from './focus-app'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -export class SetChatReminder extends APIResource { - /** - * Set a reminder for a chat at a specific time - * - * @example - * ```ts - * const baseResponse = await client.setChatReminder.create({ - * chatID: - * '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - * reminder: { remindAtMs: 0 }, - * }); - * ``` - */ - create(body: SetChatReminderCreateParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/set-chat-reminder', { body, ...options }); - } -} - -export interface SetChatReminderCreateParams { - /** - * The identifier of the chat to set reminder for - */ - chatID: string; - - /** - * Reminder configuration - */ - reminder: SetChatReminderCreateParams.Reminder; -} - -export namespace SetChatReminderCreateParams { - /** - * Reminder configuration - */ - export interface Reminder { - /** - * Unix timestamp in milliseconds when reminder should trigger - */ - remindAtMs: number; - - /** - * Cancel reminder if someone messages in the chat - */ - dismissOnIncomingMessage?: boolean; - } -} - -export declare namespace SetChatReminder { - export { type SetChatReminderCreateParams as SetChatReminderCreateParams }; -} diff --git a/src/resources/shared.ts b/src/resources/shared.ts new file mode 100644 index 0000000..491acda --- /dev/null +++ b/src/resources/shared.ts @@ -0,0 +1,173 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export interface Attachment { + /** + * Attachment type. + */ + type: 'unknown' | 'img' | 'video' | 'audio'; + + /** + * Duration in seconds (audio/video). + */ + duration?: number; + + /** + * Original filename if available. + */ + fileName?: string; + + /** + * File size in bytes if known. + */ + fileSize?: number; + + /** + * True if the attachment is a GIF. + */ + isGif?: boolean; + + /** + * True if the attachment is a sticker. + */ + isSticker?: boolean; + + /** + * True if the attachment is a voice note. + */ + isVoiceNote?: boolean; + + /** + * MIME type if known (e.g., 'image/png'). + */ + mimeType?: string; + + /** + * Preview image URL for video attachments (poster frame). May be temporary or + * local-only to this device; download promptly if durable access is needed. + */ + posterImg?: string; + + /** + * Pixel dimensions of the attachment: width/height in px. + */ + size?: Attachment.Size; + + /** + * Public URL or local file path to fetch the asset. May be temporary or local-only + * to this device; download promptly if durable access is needed. + */ + srcURL?: string; +} + +export namespace Attachment { + /** + * Pixel dimensions of the attachment: width/height in px. + */ + export interface Size { + height?: number; + + width?: number; + } +} + +export interface BaseResponse { + success: boolean; + + error?: string; +} + +export interface Error { + /** + * Error message + */ + error: string; + + /** + * Error code + */ + code?: string; + + /** + * Additional error details + */ + details?: { [key: string]: string }; +} + +export interface Reaction { + /** + * Reaction ID, typically ${participantID}${reactionKey} if multiple reactions + * allowed, or just participantID otherwise. + */ + id: string; + + /** + * User ID of the participant who reacted. + */ + participantID: string; + + /** + * The reaction key: an emoji (😄), a network-specific key, or a shortcode like + * "smiling-face". + */ + reactionKey: string; + + /** + * True if the reactionKey is an emoji. + */ + emoji?: boolean; + + /** + * URL to the reaction's image. May be temporary or local-only to this device; + * download promptly if durable access is needed. + */ + imgURL?: string; +} + +/** + * A person on or reachable through Beeper. Values are best-effort and can vary by + * network. + */ +export interface User { + /** + * Stable Beeper user ID. Use as the primary key when referencing a person. + */ + id: string; + + /** + * True if Beeper cannot initiate messages to this user (e.g., blocked, network + * restriction, or no DM path). The user may still message you. + */ + cannotMessage?: boolean; + + /** + * Email address if known. Not guaranteed verified. + */ + email?: string; + + /** + * Display name as shown in clients (e.g., 'Alice Example'). May include emojis. + */ + fullName?: string | null; + + /** + * Avatar image URL if available. May be temporary or local-only to this device; + * download promptly if durable access is needed. + */ + imgURL?: string; + + /** + * True if this user represents the authenticated account's own identity. + */ + isSelf?: boolean; + + /** + * User's phone number in E.164 format (e.g., '+14155552671'). Omit if unknown. + */ + phoneNumber?: string; + + /** + * Human-readable handle if available (e.g., '@alice'). May be network-specific and + * not globally unique. + */ + username?: string; +} diff --git a/src/version.ts b/src/version.ts index ecebcdd..1baa228 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.0.1'; +export const VERSION = '0.1.0'; // x-release-please-version diff --git a/tests/api-resources/get-accounts.test.ts b/tests/api-resources/accounts.test.ts similarity index 66% rename from tests/api-resources/get-accounts.test.ts rename to tests/api-resources/accounts.test.ts index f17d505..7d742c6 100644 --- a/tests/api-resources/get-accounts.test.ts +++ b/tests/api-resources/accounts.test.ts @@ -1,16 +1,15 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'beeper/desktop-api-typescript'; -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', +const client = new BeeperDesktop({ + accessToken: 'My Access Token', baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', }); -describe('resource getAccounts', () => { - // Prism tests are disabled - test.skip('list', async () => { - const responsePromise = client.getAccounts.list(); +describe('resource accounts', () => { + test('list', async () => { + const responsePromise = client.accounts.list(); const rawResponse = await responsePromise.asResponse(); expect(rawResponse).toBeInstanceOf(Response); const response = await responsePromise; diff --git a/tests/api-resources/focus-app.test.ts b/tests/api-resources/app.test.ts similarity index 66% rename from tests/api-resources/focus-app.test.ts rename to tests/api-resources/app.test.ts index 52d394e..e3cd36b 100644 --- a/tests/api-resources/focus-app.test.ts +++ b/tests/api-resources/app.test.ts @@ -1,16 +1,15 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'beeper/desktop-api-typescript'; -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', +const client = new BeeperDesktop({ + accessToken: 'My Access Token', baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', }); -describe('resource focusApp', () => { - // Prism tests are disabled - test.skip('open', async () => { - const responsePromise = client.focusApp.open(); +describe('resource app', () => { + test('focus', async () => { + const responsePromise = client.app.focus(); const rawResponse = await responsePromise.asResponse(); expect(rawResponse).toBeInstanceOf(Response); const response = await responsePromise; @@ -20,17 +19,16 @@ describe('resource focusApp', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled - test.skip('open: request options and params are passed correctly', async () => { + test('focus: request options and params are passed correctly', async () => { // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error await expect( - client.focusApp.open( + client.app.focus( { chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', messageSortKey: 'messageSortKey', }, { path: '/_stainless_unknown_path' }, ), - ).rejects.toThrow(BeeperDesktopAPI.NotFoundError); + ).rejects.toThrow(BeeperDesktop.NotFoundError); }); }); diff --git a/tests/api-resources/archive-chat.test.ts b/tests/api-resources/archive-chat.test.ts deleted file mode 100644 index c2db314..0000000 --- a/tests/api-resources/archive-chat.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource archiveChat', () => { - // Prism tests are disabled - test.skip('archive: only required params', async () => { - const responsePromise = client.archiveChat.archive({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - }); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('archive: required and optional params', async () => { - const response = await client.archiveChat.archive({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - archived: true, - }); - }); -}); diff --git a/tests/api-resources/chats.test.ts b/tests/api-resources/chats.test.ts new file mode 100644 index 0000000..961f94f --- /dev/null +++ b/tests/api-resources/chats.test.ts @@ -0,0 +1,104 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from 'beeper/desktop-api-typescript'; + +const client = new BeeperDesktop({ + accessToken: 'My Access Token', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource chats', () => { + test('retrieve: only required params', async () => { + const responsePromise = client.chats.retrieve({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('retrieve: required and optional params', async () => { + const response = await client.chats.retrieve({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + maxParticipantCount: 50, + }); + }); + + test('archive: only required params', async () => { + const responsePromise = client.chats.archive({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('archive: required and optional params', async () => { + const response = await client.chats.archive({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + archived: true, + }); + }); + + test('find', async () => { + const responsePromise = client.chats.find(); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('find: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.chats.find( + { + accountIDs: ['local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', 'slackgo.T031TC83W'], + ending_before: '872739', + inbox: 'primary', + includeMuted: true, + lastActivityAfter: '2019-12-27T18:11:19.117Z', + lastActivityBefore: '2019-12-27T18:11:19.117Z', + limit: 1, + participantQuery: 'participantQuery', + query: 'query', + starting_after: '196640', + type: 'single', + unreadOnly: true, + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(BeeperDesktop.NotFoundError); + }); + + test('getLink: only required params', async () => { + const responsePromise = client.chats.getLink({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('getLink: required and optional params', async () => { + const response = await client.chats.getLink({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + messageSortKey: 'messageSortKey', + }); + }); +}); diff --git a/tests/api-resources/clear-chat-reminder.test.ts b/tests/api-resources/clear-chat-reminder.test.ts deleted file mode 100644 index 7bc0456..0000000 --- a/tests/api-resources/clear-chat-reminder.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource clearChatReminder', () => { - // Prism tests are disabled - test.skip('clear: only required params', async () => { - const responsePromise = client.clearChatReminder.clear({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - }); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('clear: required and optional params', async () => { - const response = await client.clearChatReminder.clear({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - }); - }); -}); diff --git a/tests/api-resources/draft-message.test.ts b/tests/api-resources/draft-message.test.ts deleted file mode 100644 index 69b137a..0000000 --- a/tests/api-resources/draft-message.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource draftMessage', () => { - // Prism tests are disabled - test.skip('create: only required params', async () => { - const responsePromise = client.draftMessage.create({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - }); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('create: required and optional params', async () => { - const response = await client.draftMessage.create({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - focusApp: true, - text: 'text', - }); - }); -}); diff --git a/tests/api-resources/find-chats.test.ts b/tests/api-resources/find-chats.test.ts deleted file mode 100644 index 45ba304..0000000 --- a/tests/api-resources/find-chats.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource findChats', () => { - // Prism tests are disabled - test.skip('list', async () => { - const responsePromise = client.findChats.list(); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('list: request options and params are passed correctly', async () => { - // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error - await expect( - client.findChats.list( - { - accountIDs: ['local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', 'slackgo.T031TC83W'], - ending_before: '872739', - inbox: 'primary', - includeMuted: true, - lastActivityAfter: '2019-12-27T18:11:19.117Z', - lastActivityBefore: '2019-12-27T18:11:19.117Z', - limit: 1, - participantQuery: 'participantQuery', - query: 'query', - starting_after: '196640', - type: 'single', - unreadOnly: true, - }, - { path: '/_stainless_unknown_path' }, - ), - ).rejects.toThrow(BeeperDesktopAPI.NotFoundError); - }); -}); diff --git a/tests/api-resources/get-chat.test.ts b/tests/api-resources/get-chat.test.ts deleted file mode 100644 index 998a853..0000000 --- a/tests/api-resources/get-chat.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource getChat', () => { - // Prism tests are disabled - test.skip('retrieve: only required params', async () => { - const responsePromise = client.getChat.retrieve({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - }); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('retrieve: required and optional params', async () => { - const response = await client.getChat.retrieve({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - maxParticipantCount: 50, - }); - }); -}); diff --git a/tests/api-resources/get-link-to-chat.test.ts b/tests/api-resources/get-link-to-chat.test.ts deleted file mode 100644 index 2d2d05a..0000000 --- a/tests/api-resources/get-link-to-chat.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource getLinkToChat', () => { - // Prism tests are disabled - test.skip('create: only required params', async () => { - const responsePromise = client.getLinkToChat.create({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - }); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('create: required and optional params', async () => { - const response = await client.getLinkToChat.create({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - messageSortKey: 'messageSortKey', - }); - }); -}); diff --git a/tests/api-resources/messages.test.ts b/tests/api-resources/messages.test.ts new file mode 100644 index 0000000..f1976d4 --- /dev/null +++ b/tests/api-resources/messages.test.ts @@ -0,0 +1,94 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from 'beeper/desktop-api-typescript'; + +const client = new BeeperDesktop({ + accessToken: 'My Access Token', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource messages', () => { + test('draft: only required params', async () => { + const responsePromise = client.messages.draft({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('draft: required and optional params', async () => { + const response = await client.messages.draft({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + focusApp: true, + text: 'text', + }); + }); + + test('search', async () => { + const responsePromise = client.messages.search(); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('search: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.messages.search( + { + accountIDs: [ + 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + 'local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU', + ], + chatIDs: ['!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost'], + chatType: 'group', + dateAfter: '2024-07-01T00:00:00Z', + dateBefore: '2024-07-31T23:59:59Z', + ending_before: '872739', + excludeLowPriority: true, + includeMuted: true, + limit: 50, + onlyWithFile: true, + onlyWithImage: true, + onlyWithLink: true, + onlyWithMedia: true, + onlyWithVideo: true, + query: 'dinner', + sender: 'me', + starting_after: '196640', + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(BeeperDesktop.NotFoundError); + }); + + test('send: only required params', async () => { + const responsePromise = client.messages.send({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('send: required and optional params', async () => { + const response = await client.messages.send({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + replyToMessageID: 'replyToMessageID', + text: 'text', + }); + }); +}); diff --git a/tests/api-resources/oauth.test.ts b/tests/api-resources/oauth.test.ts index 0b1320e..0999bd3 100644 --- a/tests/api-resources/oauth.test.ts +++ b/tests/api-resources/oauth.test.ts @@ -1,16 +1,15 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import BeeperDesktopAPI from 'beeper-desktop-api'; +import BeeperDesktop from 'beeper/desktop-api-typescript'; -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', +const client = new BeeperDesktop({ + accessToken: 'My Access Token', baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', }); describe('resource oauth', () => { - // Prism tests are disabled - test.skip('retrieveUserInfo', async () => { - const responsePromise = client.oauth.retrieveUserInfo(); + test('getUserInfo', async () => { + const responsePromise = client.oauth.getUserInfo(); const rawResponse = await responsePromise.asResponse(); expect(rawResponse).toBeInstanceOf(Response); const response = await responsePromise; @@ -20,8 +19,7 @@ describe('resource oauth', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled - test.skip('revokeToken: only required params', async () => { + test('revokeToken: only required params', async () => { const responsePromise = client.oauth.revokeToken({ token: 'token' }); const rawResponse = await responsePromise.asResponse(); expect(rawResponse).toBeInstanceOf(Response); @@ -32,8 +30,7 @@ describe('resource oauth', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled - test.skip('revokeToken: required and optional params', async () => { + test('revokeToken: required and optional params', async () => { const response = await client.oauth.revokeToken({ token: 'token', token_type_hint: 'access_token' }); }); }); diff --git a/tests/api-resources/reminders.test.ts b/tests/api-resources/reminders.test.ts new file mode 100644 index 0000000..f29fa36 --- /dev/null +++ b/tests/api-resources/reminders.test.ts @@ -0,0 +1,50 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from 'beeper/desktop-api-typescript'; + +const client = new BeeperDesktop({ + accessToken: 'My Access Token', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource reminders', () => { + test('clear: only required params', async () => { + const responsePromise = client.reminders.clear({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('clear: required and optional params', async () => { + const response = await client.reminders.clear({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + }); + }); + + test('set: only required params', async () => { + const responsePromise = client.reminders.set({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + reminder: { remindAtMs: 0 }, + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('set: required and optional params', async () => { + const response = await client.reminders.set({ + chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', + reminder: { remindAtMs: 0, dismissOnIncomingMessage: true }, + }); + }); +}); diff --git a/tests/api-resources/search-messages.test.ts b/tests/api-resources/search-messages.test.ts deleted file mode 100644 index 59494f6..0000000 --- a/tests/api-resources/search-messages.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource searchMessages', () => { - // Prism tests are disabled - test.skip('search', async () => { - const responsePromise = client.searchMessages.search(); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('search: request options and params are passed correctly', async () => { - // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error - await expect( - client.searchMessages.search( - { - accountIDs: [ - 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', - 'local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU', - ], - chatIDs: ['!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost'], - chatType: 'group', - dateAfter: '2024-07-01T00:00:00Z', - dateBefore: '2024-07-31T23:59:59Z', - ending_before: '872739', - excludeLowPriority: true, - includeMuted: true, - limit: 50, - onlyWithFile: true, - onlyWithImage: true, - onlyWithLink: true, - onlyWithMedia: true, - onlyWithVideo: true, - query: 'dinner', - sender: 'me', - starting_after: '196640', - }, - { path: '/_stainless_unknown_path' }, - ), - ).rejects.toThrow(BeeperDesktopAPI.NotFoundError); - }); -}); diff --git a/tests/api-resources/send-message.test.ts b/tests/api-resources/send-message.test.ts deleted file mode 100644 index eabec02..0000000 --- a/tests/api-resources/send-message.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource sendMessage', () => { - // Prism tests are disabled - test.skip('send: only required params', async () => { - const responsePromise = client.sendMessage.send({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - }); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('send: required and optional params', async () => { - const response = await client.sendMessage.send({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - replyToMessageID: 'replyToMessageID', - text: 'text', - }); - }); -}); diff --git a/tests/api-resources/set-chat-reminder.test.ts b/tests/api-resources/set-chat-reminder.test.ts deleted file mode 100644 index 75532f3..0000000 --- a/tests/api-resources/set-chat-reminder.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktopAPI from 'beeper-desktop-api'; - -const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource setChatReminder', () => { - // Prism tests are disabled - test.skip('create: only required params', async () => { - const responsePromise = client.setChatReminder.create({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - reminder: { remindAtMs: 0 }, - }); - const rawResponse = await responsePromise.asResponse(); - expect(rawResponse).toBeInstanceOf(Response); - const response = await responsePromise; - expect(response).not.toBeInstanceOf(Response); - const dataAndResponse = await responsePromise.withResponse(); - expect(dataAndResponse.data).toBe(response); - expect(dataAndResponse.response).toBe(rawResponse); - }); - - // Prism tests are disabled - test.skip('create: required and optional params', async () => { - const response = await client.setChatReminder.create({ - chatID: '!-5hI_iHR5vSDCtI8PzSDQT0H_3I:ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc.local-whatsapp.localhost', - reminder: { remindAtMs: 0, dismissOnIncomingMessage: true }, - }); - }); -}); diff --git a/tests/base64.test.ts b/tests/base64.test.ts index 9a46615..fe1a0cc 100644 --- a/tests/base64.test.ts +++ b/tests/base64.test.ts @@ -1,4 +1,4 @@ -import { fromBase64, toBase64 } from 'beeper-desktop-api/internal/utils/base64'; +import { fromBase64, toBase64 } from 'desktop-api-typescript/internal/utils/base64'; describe.each(['Buffer', 'atob'])('with %s', (mode) => { let originalBuffer: BufferConstructor; diff --git a/tests/buildHeaders.test.ts b/tests/buildHeaders.test.ts index 3f64bed..ef4cf97 100644 --- a/tests/buildHeaders.test.ts +++ b/tests/buildHeaders.test.ts @@ -1,5 +1,9 @@ import { inspect } from 'node:util'; -import { buildHeaders, type HeadersLike, type NullableHeaders } from 'beeper-desktop-api/internal/headers'; +import { + buildHeaders, + type HeadersLike, + type NullableHeaders, +} from 'desktop-api-typescript/internal/headers'; function inspectNullableHeaders(headers: NullableHeaders) { return `NullableHeaders {${[ diff --git a/tests/form.test.ts b/tests/form.test.ts index c782e0d..69f2595 100644 --- a/tests/form.test.ts +++ b/tests/form.test.ts @@ -1,5 +1,5 @@ -import { multipartFormRequestOptions, createForm } from 'beeper-desktop-api/internal/uploads'; -import { toFile } from 'beeper-desktop-api/core/uploads'; +import { multipartFormRequestOptions, createForm } from 'desktop-api-typescript/internal/uploads'; +import { toFile } from 'desktop-api-typescript/core/uploads'; describe('form data validation', () => { test('valid values do not error', async () => { diff --git a/tests/index.test.ts b/tests/index.test.ts index ee08ca6..873348d 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,10 +1,10 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { APIPromise } from 'beeper-desktop-api/core/api-promise'; +import { APIPromise } from 'beeper/desktop-api-typescript/core/api-promise'; import util from 'node:util'; -import BeeperDesktopAPI from 'beeper-desktop-api'; -import { APIUserAbortError } from 'beeper-desktop-api'; +import BeeperDesktop from 'beeper/desktop-api-typescript'; +import { APIUserAbortError } from 'beeper/desktop-api-typescript'; const defaultFetch = fetch; describe('instantiate client', () => { @@ -20,10 +20,10 @@ describe('instantiate client', () => { }); describe('defaultHeaders', () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', defaultHeaders: { 'X-My-Default-Header': '2' }, - apiKey: 'My API Key', + accessToken: 'My Access Token', }); test('they are used in the request', async () => { @@ -54,14 +54,14 @@ describe('instantiate client', () => { beforeEach(() => { process.env = { ...env }; - process.env['BEEPER_DESKTOP_API_LOG'] = undefined; + process.env['BEEPER-DESKTOP_LOG'] = undefined; }); afterEach(() => { process.env = env; }); - const forceAPIResponseForClient = async (client: BeeperDesktopAPI) => { + const forceAPIResponseForClient = async (client: BeeperDesktop) => { await new APIPromise( client, Promise.resolve({ @@ -87,14 +87,14 @@ describe('instantiate client', () => { error: jest.fn(), }; - const client = new BeeperDesktopAPI({ logger: logger, logLevel: 'debug', apiKey: 'My API Key' }); + const client = new BeeperDesktop({ logger: logger, logLevel: 'debug', accessToken: 'My Access Token' }); await forceAPIResponseForClient(client); expect(debugMock).toHaveBeenCalled(); }); test('default logLevel is warn', async () => { - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); expect(client.logLevel).toBe('warn'); }); @@ -107,7 +107,7 @@ describe('instantiate client', () => { error: jest.fn(), }; - const client = new BeeperDesktopAPI({ logger: logger, logLevel: 'info', apiKey: 'My API Key' }); + const client = new BeeperDesktop({ logger: logger, logLevel: 'info', accessToken: 'My Access Token' }); await forceAPIResponseForClient(client); expect(debugMock).not.toHaveBeenCalled(); @@ -122,8 +122,8 @@ describe('instantiate client', () => { error: jest.fn(), }; - process.env['BEEPER_DESKTOP_API_LOG'] = 'debug'; - const client = new BeeperDesktopAPI({ logger: logger, apiKey: 'My API Key' }); + process.env['BEEPER-DESKTOP_LOG'] = 'debug'; + const client = new BeeperDesktop({ logger: logger, accessToken: 'My Access Token' }); expect(client.logLevel).toBe('debug'); await forceAPIResponseForClient(client); @@ -139,11 +139,11 @@ describe('instantiate client', () => { error: jest.fn(), }; - process.env['BEEPER_DESKTOP_API_LOG'] = 'not a log level'; - const client = new BeeperDesktopAPI({ logger: logger, apiKey: 'My API Key' }); + process.env['BEEPER-DESKTOP_LOG'] = 'not a log level'; + const client = new BeeperDesktop({ logger: logger, accessToken: 'My Access Token' }); expect(client.logLevel).toBe('warn'); expect(warnMock).toHaveBeenCalledWith( - 'process.env[\'BEEPER_DESKTOP_API_LOG\'] was set to "not a log level", expected one of ["off","error","warn","info","debug"]', + 'process.env[\'BEEPER-DESKTOP_LOG\'] was set to "not a log level", expected one of ["off","error","warn","info","debug"]', ); }); @@ -156,8 +156,8 @@ describe('instantiate client', () => { error: jest.fn(), }; - process.env['BEEPER_DESKTOP_API_LOG'] = 'debug'; - const client = new BeeperDesktopAPI({ logger: logger, logLevel: 'off', apiKey: 'My API Key' }); + process.env['BEEPER-DESKTOP_LOG'] = 'debug'; + const client = new BeeperDesktop({ logger: logger, logLevel: 'off', accessToken: 'My Access Token' }); await forceAPIResponseForClient(client); expect(debugMock).not.toHaveBeenCalled(); @@ -172,8 +172,8 @@ describe('instantiate client', () => { error: jest.fn(), }; - process.env['BEEPER_DESKTOP_API_LOG'] = 'not a log level'; - const client = new BeeperDesktopAPI({ logger: logger, logLevel: 'debug', apiKey: 'My API Key' }); + process.env['BEEPER-DESKTOP_LOG'] = 'not a log level'; + const client = new BeeperDesktop({ logger: logger, logLevel: 'debug', accessToken: 'My Access Token' }); expect(client.logLevel).toBe('debug'); expect(warnMock).not.toHaveBeenCalled(); }); @@ -181,37 +181,37 @@ describe('instantiate client', () => { describe('defaultQuery', () => { test('with null query params given', () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', defaultQuery: { apiVersion: 'foo' }, - apiKey: 'My API Key', + accessToken: 'My Access Token', }); expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/foo?apiVersion=foo'); }); test('multiple default query params', () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', defaultQuery: { apiVersion: 'foo', hello: 'world' }, - apiKey: 'My API Key', + accessToken: 'My Access Token', }); expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/foo?apiVersion=foo&hello=world'); }); test('overriding with `undefined`', () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', defaultQuery: { hello: 'world' }, - apiKey: 'My API Key', + accessToken: 'My Access Token', }); expect(client.buildURL('/foo', { hello: undefined })).toEqual('http://localhost:5000/foo'); }); }); test('custom fetch', async () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', - apiKey: 'My API Key', + accessToken: 'My Access Token', fetch: (url) => { return Promise.resolve( new Response(JSON.stringify({ url, custom: true }), { @@ -227,17 +227,17 @@ describe('instantiate client', () => { test('explicit global fetch', async () => { // make sure the global fetch type is assignable to our Fetch type - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', - apiKey: 'My API Key', + accessToken: 'My Access Token', fetch: defaultFetch, }); }); test('custom signal', async () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', - apiKey: 'My API Key', + accessToken: 'My Access Token', fetch: (...args) => { return new Promise((resolve, reject) => setTimeout( @@ -267,9 +267,9 @@ describe('instantiate client', () => { return new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' } }); }; - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', - apiKey: 'My API Key', + accessToken: 'My Access Token', fetch: testFetch, }); @@ -279,65 +279,68 @@ describe('instantiate client', () => { describe('baseUrl', () => { test('trailing slash', () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/custom/path/', - apiKey: 'My API Key', + accessToken: 'My Access Token', }); expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/custom/path/foo'); }); test('no trailing slash', () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/custom/path', - apiKey: 'My API Key', + accessToken: 'My Access Token', }); expect(client.buildURL('/foo', null)).toEqual('http://localhost:5000/custom/path/foo'); }); afterEach(() => { - process.env['BEEPER_DESKTOP_API_BASE_URL'] = undefined; + process.env['BEEPER-DESKTOP_BASE_URL'] = undefined; }); test('explicit option', () => { - const client = new BeeperDesktopAPI({ baseURL: 'https://example.com', apiKey: 'My API Key' }); + const client = new BeeperDesktop({ baseURL: 'https://example.com', accessToken: 'My Access Token' }); expect(client.baseURL).toEqual('https://example.com'); }); test('env variable', () => { - process.env['BEEPER_DESKTOP_API_BASE_URL'] = 'https://example.com/from_env'; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); + process.env['BEEPER-DESKTOP_BASE_URL'] = 'https://example.com/from_env'; + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); expect(client.baseURL).toEqual('https://example.com/from_env'); }); test('empty env variable', () => { - process.env['BEEPER_DESKTOP_API_BASE_URL'] = ''; // empty - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); + process.env['BEEPER-DESKTOP_BASE_URL'] = ''; // empty + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); expect(client.baseURL).toEqual('http://localhost:23374'); }); test('blank env variable', () => { - process.env['BEEPER_DESKTOP_API_BASE_URL'] = ' '; // blank - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); + process.env['BEEPER-DESKTOP_BASE_URL'] = ' '; // blank + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); expect(client.baseURL).toEqual('http://localhost:23374'); }); test('in request options', () => { - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( 'http://localhost:5000/option/foo', ); }); test('in request options overridden by client options', () => { - const client = new BeeperDesktopAPI({ apiKey: 'My API Key', baseURL: 'http://localhost:5000/client' }); + const client = new BeeperDesktop({ + accessToken: 'My Access Token', + baseURL: 'http://localhost:5000/client', + }); expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( 'http://localhost:5000/client/foo', ); }); test('in request options overridden by env variable', () => { - process.env['BEEPER_DESKTOP_API_BASE_URL'] = 'http://localhost:5000/env'; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); + process.env['BEEPER-DESKTOP_BASE_URL'] = 'http://localhost:5000/env'; + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( 'http://localhost:5000/env/foo', ); @@ -345,20 +348,20 @@ describe('instantiate client', () => { }); test('maxRetries option is correctly set', () => { - const client = new BeeperDesktopAPI({ maxRetries: 4, apiKey: 'My API Key' }); - expect(client.maxRetries).toEqual(4); + const client = new BeeperDesktop({ maxRetries: 6, accessToken: 'My Access Token' }); + expect(client.maxRetries).toEqual(6); // default - const client2 = new BeeperDesktopAPI({ apiKey: 'My API Key' }); - expect(client2.maxRetries).toEqual(2); + const client2 = new BeeperDesktop({ accessToken: 'My Access Token' }); + expect(client2.maxRetries).toEqual(3); }); describe('withOptions', () => { test('creates a new client with overridden options', async () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', maxRetries: 3, - apiKey: 'My API Key', + accessToken: 'My Access Token', }); const newClient = client.withOptions({ @@ -380,11 +383,11 @@ describe('instantiate client', () => { }); test('inherits options from the parent client', async () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', defaultHeaders: { 'X-Test-Header': 'test-value' }, defaultQuery: { 'test-param': 'test-value' }, - apiKey: 'My API Key', + accessToken: 'My Access Token', }); const newClient = client.withOptions({ @@ -399,10 +402,10 @@ describe('instantiate client', () => { }); test('respects runtime property changes when creating new client', () => { - const client = new BeeperDesktopAPI({ + const client = new BeeperDesktop({ baseURL: 'http://localhost:5000/', timeout: 1000, - apiKey: 'My API Key', + accessToken: 'My Access Token', }); // Modify the client properties directly after creation @@ -431,21 +434,21 @@ describe('instantiate client', () => { test('with environment variable arguments', () => { // set options via env var - process.env['BEEPER_DESKTOP_API_API_KEY'] = 'My API Key'; - const client = new BeeperDesktopAPI(); - expect(client.apiKey).toBe('My API Key'); + process.env['BEEPER_ACCESS_TOKEN'] = 'My Access Token'; + const client = new BeeperDesktop(); + expect(client.accessToken).toBe('My Access Token'); }); test('with overridden environment variable arguments', () => { // set options via env var - process.env['BEEPER_DESKTOP_API_API_KEY'] = 'another My API Key'; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); - expect(client.apiKey).toBe('My API Key'); + process.env['BEEPER_ACCESS_TOKEN'] = 'another My Access Token'; + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); + expect(client.accessToken).toBe('My Access Token'); }); }); describe('request building', () => { - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); describe('custom headers', () => { test('handles undefined', async () => { @@ -464,7 +467,7 @@ describe('request building', () => { }); describe('default encoder', () => { - const client = new BeeperDesktopAPI({ apiKey: 'My API Key' }); + const client = new BeeperDesktop({ accessToken: 'My Access Token' }); class Serializable { toJSON() { @@ -549,7 +552,7 @@ describe('retries', () => { return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } }); }; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key', timeout: 10, fetch: testFetch }); + const client = new BeeperDesktop({ accessToken: 'My Access Token', timeout: 10, fetch: testFetch }); expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 }); expect(count).toEqual(2); @@ -579,7 +582,7 @@ describe('retries', () => { return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } }); }; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key', fetch: testFetch, maxRetries: 4 }); + const client = new BeeperDesktop({ accessToken: 'My Access Token', fetch: testFetch, maxRetries: 4 }); expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 }); @@ -603,7 +606,7 @@ describe('retries', () => { capturedRequest = init; return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } }); }; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key', fetch: testFetch, maxRetries: 4 }); + const client = new BeeperDesktop({ accessToken: 'My Access Token', fetch: testFetch, maxRetries: 4 }); expect( await client.request({ @@ -632,8 +635,8 @@ describe('retries', () => { capturedRequest = init; return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } }); }; - const client = new BeeperDesktopAPI({ - apiKey: 'My API Key', + const client = new BeeperDesktop({ + accessToken: 'My Access Token', fetch: testFetch, maxRetries: 4, defaultHeaders: { 'X-Stainless-Retry-Count': null }, @@ -665,7 +668,7 @@ describe('retries', () => { capturedRequest = init; return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } }); }; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key', fetch: testFetch, maxRetries: 4 }); + const client = new BeeperDesktop({ accessToken: 'My Access Token', fetch: testFetch, maxRetries: 4 }); expect( await client.request({ @@ -695,7 +698,7 @@ describe('retries', () => { return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } }); }; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key', fetch: testFetch }); + const client = new BeeperDesktop({ accessToken: 'My Access Token', fetch: testFetch }); expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 }); expect(count).toEqual(2); @@ -725,7 +728,7 @@ describe('retries', () => { return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } }); }; - const client = new BeeperDesktopAPI({ apiKey: 'My API Key', fetch: testFetch }); + const client = new BeeperDesktop({ accessToken: 'My Access Token', fetch: testFetch }); expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 }); expect(count).toEqual(2); diff --git a/tests/path.test.ts b/tests/path.test.ts index f224354..e793ba6 100644 --- a/tests/path.test.ts +++ b/tests/path.test.ts @@ -1,4 +1,4 @@ -import { createPathTagFunction, encodeURIPath } from 'beeper-desktop-api/internal/utils/path'; +import { createPathTagFunction, encodeURIPath } from 'desktop-api-typescript/internal/utils/path'; import { inspect } from 'node:util'; import { runInNewContext } from 'node:vm'; diff --git a/tests/qs/stringify.test.ts b/tests/qs/stringify.test.ts index 4d6f90c..3c14ba8 100644 --- a/tests/qs/stringify.test.ts +++ b/tests/qs/stringify.test.ts @@ -1,7 +1,7 @@ import iconv from 'iconv-lite'; -import { stringify } from 'beeper-desktop-api/internal/qs'; -import { encode } from 'beeper-desktop-api/internal/qs/utils'; -import { StringifyOptions } from 'beeper-desktop-api/internal/qs/types'; +import { stringify } from 'desktop-api-typescript/internal/qs'; +import { encode } from 'desktop-api-typescript/internal/qs/utils'; +import { StringifyOptions } from 'desktop-api-typescript/internal/qs/types'; import { empty_test_cases } from './empty-keys-cases'; import assert from 'assert'; diff --git a/tests/qs/utils.test.ts b/tests/qs/utils.test.ts index dde0023..11bd119 100644 --- a/tests/qs/utils.test.ts +++ b/tests/qs/utils.test.ts @@ -1,4 +1,4 @@ -import { combine, merge, is_buffer, assign_single_source } from 'beeper-desktop-api/internal/qs/utils'; +import { combine, merge, is_buffer, assign_single_source } from 'desktop-api-typescript/internal/qs/utils'; describe('merge()', function () { // t.deepEqual(merge(null, true), [null, true], 'merges true into null'); diff --git a/tests/stringifyQuery.test.ts b/tests/stringifyQuery.test.ts index f2c3ee1..652a41b 100644 --- a/tests/stringifyQuery.test.ts +++ b/tests/stringifyQuery.test.ts @@ -1,8 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { BeeperDesktopAPI } from 'beeper-desktop-api'; +import { BeeperDesktop } from 'beeper/desktop-api-typescript'; -const { stringifyQuery } = BeeperDesktopAPI.prototype as any; +const { stringifyQuery } = BeeperDesktop.prototype as any; describe(stringifyQuery, () => { for (const [input, expected] of [ diff --git a/tests/uploads.test.ts b/tests/uploads.test.ts index fa3d332..44abcb8 100644 --- a/tests/uploads.test.ts +++ b/tests/uploads.test.ts @@ -1,6 +1,6 @@ import fs from 'fs'; -import type { ResponseLike } from 'beeper-desktop-api/internal/to-file'; -import { toFile } from 'beeper-desktop-api/core/uploads'; +import type { ResponseLike } from 'desktop-api-typescript/internal/to-file'; +import { toFile } from 'desktop-api-typescript/core/uploads'; import { File } from 'node:buffer'; class MyClass { @@ -97,7 +97,7 @@ describe('missing File error message', () => { }); test('is thrown', async () => { - const uploads = await import('beeper-desktop-api/core/uploads'); + const uploads = await import('desktop-api-typescript/core/uploads'); await expect( uploads.toFile(mockResponse({ url: 'https://example.com/my/audio.mp3' })), ).rejects.toMatchInlineSnapshot( diff --git a/tsconfig.build.json b/tsconfig.build.json index 2627356..45a574e 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,8 +5,8 @@ "compilerOptions": { "rootDir": "./dist/src", "paths": { - "beeper-desktop-api/*": ["dist/src/*"], - "beeper-desktop-api": ["dist/src/index.ts"] + "desktop-api-typescript/*": ["dist/src/*"], + "desktop-api-typescript": ["dist/src/index.ts"] }, "noEmit": false, "declaration": true, diff --git a/tsconfig.json b/tsconfig.json index 8d66644..c8aa59c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,8 @@ "esModuleInterop": true, "baseUrl": "./", "paths": { - "beeper-desktop-api/*": ["src/*"], - "beeper-desktop-api": ["src/index.ts"] + "desktop-api-typescript/*": ["src/*"], + "desktop-api-typescript": ["src/index.ts"] }, "noEmit": true,