From cd62f83fc5e6e93501df72cf49c0ee2838e02476 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 02:55:46 +0000 Subject: [PATCH 01/36] chore: do not install brew dependencies in ./scripts/bootstrap by default --- scripts/bootstrap | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index 062a034..a8b69ff 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,10 +4,18 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { - echo "==> Installing Homebrew dependencies…" - brew bundle + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo } fi From c349699a05745356ce3d12dc693d643a85a52d5d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 02:33:27 +0000 Subject: [PATCH 02/36] feat(mcp): enable experimental docs search tool --- packages/mcp-server/src/docs-search-tool.ts | 47 +++++++++++++++++++++ packages/mcp-server/src/server.ts | 3 +- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 packages/mcp-server/src/docs-search-tool.ts diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts new file mode 100644 index 0000000..81e6705 --- /dev/null +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -0,0 +1,47 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from './tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; + +export const metadata: Metadata = { + resource: 'all', + operation: 'read', + tags: [], + httpMethod: 'get', +}; + +export const tool: Tool = { + name: 'search_docs', + description: + 'Search for documentation for how to use the client to interact with the API.\nThe tool will return an array of Markdown-formatted documentation pages.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The query to search for.', + }, + language: { + type: 'string', + description: 'The language for the SDK to search for.', + enum: ['http', 'python', 'go', 'typescript', 'terraform', 'ruby', 'java', 'kotlin'], + }, + }, + required: ['query', 'language'], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (_: unknown, args: Record | undefined) => { + const body = args as any; + const query = new URLSearchParams(body).toString(); + const result = await fetch( + 'https://api.stainless.com/api/projects/beeper-desktop-api/docs/search?' + query, + ); + return asTextContentResult(await result.json()); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 3f193eb..278370d 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -21,6 +21,7 @@ import { } from './compat'; import { dynamicTools } from './dynamic-tools'; import { codeTool } from './code-tool'; +import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; export { McpOptions } from './options'; @@ -163,7 +164,7 @@ export async function selectTools(endpoints: Endpoint[], options?: McpOptions): } else if (options?.includeDynamicTools) { includedTools = dynamicTools(endpoints); } else if (options?.includeCodeTools) { - includedTools = [await codeTool()]; + includedTools = [await codeTool(), docsSearchTool]; } else { includedTools = endpoints; } From a521cb8f972778c0253b657b712da7681dd78853 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 02:49:32 +0000 Subject: [PATCH 03/36] feat(mcp): add option for including docs tools --- packages/mcp-server/src/options.ts | 19 ++++++++++++++----- packages/mcp-server/src/server.ts | 12 +++++++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index ecc9f10..348a33d 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -17,6 +17,7 @@ export type McpOptions = { includeDynamicTools?: boolean | undefined; includeAllTools?: boolean | undefined; includeCodeTools?: boolean | undefined; + includeDocsTools?: boolean | undefined; filters?: Filter[] | undefined; capabilities?: Partial | undefined; }; @@ -55,13 +56,13 @@ export function parseCLIOptions(): CLIOptions { .option('tools', { type: 'string', array: true, - choices: ['dynamic', 'all', 'code'], + choices: ['dynamic', 'all', 'code', 'docs'], description: 'Use dynamic tools or all tools', }) .option('no-tools', { type: 'string', array: true, - choices: ['dynamic', 'all', 'code'], + choices: ['dynamic', 'all', 'code', 'docs'], description: 'Do not use any dynamic or all tools', }) .option('tool', { @@ -245,13 +246,14 @@ export function parseCLIOptions(): CLIOptions { } } - const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code') => + const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code' | 'docs') => 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 includeDocsTools = shouldIncludeToolType('docs'); const transport = argv.transport as 'stdio' | 'http'; @@ -261,6 +263,7 @@ export function parseCLIOptions(): CLIOptions { includeDynamicTools, includeAllTools, includeCodeTools, + includeDocsTools, filters, capabilities: clientCapabilities, list: argv.list || false, @@ -280,8 +283,8 @@ const coerceArray = (zodType: T) => ); 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'), + tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Use dynamic tools or all tools'), + no_tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).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( @@ -376,11 +379,17 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M : queryOptions.tools?.includes('all') ? true : defaultOptions.includeAllTools; + let docsTools: boolean | undefined = + queryOptions.no_tools && queryOptions.no_tools?.includes('docs') ? false + : queryOptions.tools?.includes('docs') ? true + : defaultOptions.includeDocsTools; + return { client: queryOptions.client ?? defaultOptions.client, includeDynamicTools: dynamicTools, includeAllTools: allTools, includeCodeTools: undefined, + includeDocsTools: docsTools, filters, capabilities: clientCapabilities, }; diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 278370d..5b30ecb 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -152,7 +152,7 @@ export function initMcpServer(params: { export async function selectTools(endpoints: Endpoint[], options?: McpOptions): Promise { const filteredEndpoints = query(options?.filters ?? [], endpoints); - let includedTools = filteredEndpoints; + let includedTools = filteredEndpoints.slice(); if (includedTools.length > 0) { if (options?.includeDynamicTools) { @@ -160,16 +160,18 @@ export async function selectTools(endpoints: Endpoint[], options?: McpOptions): } } else { if (options?.includeAllTools) { - includedTools = endpoints; + includedTools = endpoints.slice(); } else if (options?.includeDynamicTools) { includedTools = dynamicTools(endpoints); } else if (options?.includeCodeTools) { - includedTools = [await codeTool(), docsSearchTool]; + includedTools = [await codeTool()]; } else { - includedTools = endpoints; + includedTools = endpoints.slice(); } } - + if (options?.includeDocsTools ?? true) { + includedTools.push(docsSearchTool); + } const capabilities = { ...defaultClientCapabilities, ...options?.capabilities }; return applyCompatibilityTransformations(includedTools, capabilities); } From a4321bf9c91f3043414724486c130f3b0bd25606 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 02:27:38 +0000 Subject: [PATCH 04/36] perf: faster formatting --- scripts/fast-format | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100755 scripts/fast-format diff --git a/scripts/fast-format b/scripts/fast-format new file mode 100755 index 0000000..ef42df5 --- /dev/null +++ b/scripts/fast-format @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "Script started with $# arguments" +echo "Arguments: $*" +echo "Script location: $(dirname "$0")" + +cd "$(dirname "$0")/.." +echo "Changed to directory: $(pwd)" + +if [ $# -eq 0 ]; then + echo "Usage: $0 [additional-formatter-args...]" + echo "The file should contain one file path per line" + exit 1 +fi + +FILE_LIST="$1" + +echo "Looking for file: $FILE_LIST" + +if [ ! -f "$FILE_LIST" ]; then + echo "Error: File '$FILE_LIST' not found" + exit 1 +fi + +echo "==> Running eslint --fix" +ESLINT_FILES="$(grep '\.ts$' "$FILE_LIST" || true)" +if ! [ -z "$ESLINT_FILES" ]; then + echo "$ESLINT_FILES" | xargs ./node_modules/.bin/eslint --cache --fix +fi + +echo "==> Running prettier --write" +# format things eslint didn't +PRETTIER_FILES="$(grep '\.\(js\|json\)$' "$FILE_LIST" || true)" +if ! [ -z "$PRETTIER_FILES" ]; then + echo "$PRETTIER_FILES" | xargs ./node_modules/.bin/prettier \ + --write --cache --cache-strategy metadata \ + '!**/dist' '!**/*.ts' '!**/*.mts' '!**/*.cts' '!**/*.js' '!**/*.mjs' '!**/*.cjs' +fi From ffaa6d646236e88ecf997dc9a0b77733029a885b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 02:28:42 +0000 Subject: [PATCH 05/36] chore(internal): remove deprecated `compilerOptions.baseUrl` from tsconfig.json This allows sdks to be built using tsgo - see https://github.com/microsoft/typescript-go/issues/474 --- packages/mcp-server/tsconfig.build.json | 4 ++-- packages/mcp-server/tsconfig.json | 5 ++--- tsconfig.build.json | 4 ++-- tsconfig.json | 5 ++--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/mcp-server/tsconfig.build.json b/packages/mcp-server/tsconfig.build.json index e790c29..c1fe977 100644 --- a/packages/mcp-server/tsconfig.build.json +++ b/packages/mcp-server/tsconfig.build.json @@ -5,8 +5,8 @@ "compilerOptions": { "rootDir": "./dist/src", "paths": { - "@beeper/desktop-mcp/*": ["dist/src/*"], - "@beeper/desktop-mcp": ["dist/src/index.ts"] + "@beeper/desktop-mcp/*": ["./dist/src/*"], + "@beeper/desktop-mcp": ["./dist/src/index.ts"] }, "noEmit": false, "declaration": true, diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index ad28452..c70b6cc 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -7,10 +7,9 @@ "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, - "baseUrl": "./", "paths": { - "@beeper/desktop-mcp/*": ["src/*"], - "@beeper/desktop-mcp": ["src/index.ts"] + "@beeper/desktop-mcp/*": ["./src/*"], + "@beeper/desktop-mcp": ["./src/index.ts"] }, "noEmit": true, diff --git a/tsconfig.build.json b/tsconfig.build.json index 4dd4578..1954cb1 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"] + "@beeper/desktop-api/*": ["./dist/src/*"], + "@beeper/desktop-api": ["./dist/src/index.ts"] }, "noEmit": false, "declaration": true, diff --git a/tsconfig.json b/tsconfig.json index be8a0cd..a69f508 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,10 +7,9 @@ "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, - "baseUrl": "./", "paths": { - "@beeper/desktop-api/*": ["src/*"], - "@beeper/desktop-api": ["src/index.ts"] + "@beeper/desktop-api/*": ["./src/*"], + "@beeper/desktop-api": ["./src/index.ts"] }, "noEmit": true, From 697d742608fead14c628f5d809b6a4beedff4656 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 02:38:22 +0000 Subject: [PATCH 06/36] chore(internal): fix incremental formatting in some cases --- scripts/fast-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fast-format b/scripts/fast-format index ef42df5..53721ac 100755 --- a/scripts/fast-format +++ b/scripts/fast-format @@ -35,6 +35,6 @@ echo "==> Running prettier --write" PRETTIER_FILES="$(grep '\.\(js\|json\)$' "$FILE_LIST" || true)" if ! [ -z "$PRETTIER_FILES" ]; then echo "$PRETTIER_FILES" | xargs ./node_modules/.bin/prettier \ - --write --cache --cache-strategy metadata \ + --write --cache --cache-strategy metadata --no-error-on-unmatched-pattern \ '!**/dist' '!**/*.ts' '!**/*.mts' '!**/*.cts' '!**/*.js' '!**/*.mjs' '!**/*.cjs' fi From 6044c3467dd578e2fba46fa10f83ad3240222c3f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 02:39:34 +0000 Subject: [PATCH 07/36] chore(mcp): allow pointing `docs_search` tool at other URLs --- .eslintcache | 1 + packages/mcp-server/src/docs-search-tool.ts | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .eslintcache diff --git a/.eslintcache b/.eslintcache new file mode 100644 index 0000000..e35f169 --- /dev/null +++ b/.eslintcache @@ -0,0 +1 @@ +[{"/home/tempuser-7buwh2/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/docs-search-tool.ts":"1"},{"size":1472,"mtime":1758940769742}] \ No newline at end of file diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 81e6705..eb89849 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -35,12 +35,13 @@ export const tool: Tool = { }, }; +const docsSearchURL = + process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/beeper-desktop-api/docs/search'; + export const handler = async (_: unknown, args: Record | undefined) => { const body = args as any; const query = new URLSearchParams(body).toString(); - const result = await fetch( - 'https://api.stainless.com/api/projects/beeper-desktop-api/docs/search?' + query, - ); + const result = await fetch(`${docsSearchURL}?${query}`); return asTextContentResult(await result.json()); }; From 44137d644ffcd7407acc815a68d8df093e17f45d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 02:41:59 +0000 Subject: [PATCH 08/36] chore(internal): codegen related update --- .eslintcache | 2 +- .github/workflows/release-doctor.yml | 1 + packages/mcp-server/src/code-tool-paths.cts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintcache b/.eslintcache index e35f169..2798f1b 100644 --- a/.eslintcache +++ b/.eslintcache @@ -1 +1 @@ -[{"/home/tempuser-7buwh2/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/docs-search-tool.ts":"1"},{"size":1472,"mtime":1758940769742}] \ No newline at end of file +[{"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/jest.config.ts":"1","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/jest.config.ts":"2","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/code-tool-types.ts":"3","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/code-tool-worker.ts":"4","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/code-tool.ts":"5","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/compat.ts":"6","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/docs-search-tool.ts":"7","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/dynamic-tools.ts":"8","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/filtering.ts":"9","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/headers.ts":"10","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/http.ts":"11","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/index.ts":"12","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/options.ts":"13","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/server.ts":"14","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/stdio.ts":"15","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/accounts/get-accounts.ts":"16","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/app/open-in-app.ts":"17","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/app/search.ts":"18","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/get-chat.ts":"19","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/archive-chat.ts":"20","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts":"21","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/index.ts":"22","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts":"23","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/search-chats.ts":"24","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/messages/search-messages.ts":"25","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/messages/send-message.ts":"26","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/types.ts":"27","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools.ts":"28","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/dynamic-tools.test.ts":"29","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/compat.test.ts":"30","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/options.test.ts":"31","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/tools.test.ts":"32","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/scripts/publish-packages.ts":"33","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/api-promise.ts":"34","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/client.ts":"35","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/api-promise.ts":"36","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/error.ts":"37","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/pagination.ts":"38","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/resource.ts":"39","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/uploads.ts":"40","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/error.ts":"41","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/index.ts":"42","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/builtin-types.ts":"43","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/detect-platform.ts":"44","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/errors.ts":"45","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/headers.ts":"46","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/parse.ts":"47","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/formats.ts":"48","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/index.ts":"49","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/stringify.ts":"50","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/types.ts":"51","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/utils.ts":"52","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/request-options.ts":"53","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/shim-types.ts":"54","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/shims.ts":"55","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/to-file.ts":"56","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/types.ts":"57","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/uploads.ts":"58","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/base64.ts":"59","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/bytes.ts":"60","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/env.ts":"61","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/log.ts":"62","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/path.ts":"63","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/sleep.ts":"64","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/uuid.ts":"65","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/values.ts":"66","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils.ts":"67","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/pagination.ts":"68","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resource.ts":"69","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/accounts.ts":"70","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/app.ts":"71","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats/chats.ts":"72","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats/index.ts":"73","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats/reminders.ts":"74","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats.ts":"75","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/contacts.ts":"76","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/messages.ts":"77","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/index.ts":"78","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/shared.ts":"79","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/token.ts":"80","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources.ts":"81","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/uploads.ts":"82","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/version.ts":"83","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/accounts.test.ts":"84","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/app.test.ts":"85","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/chats/chats.test.ts":"86","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/chats/reminders.test.ts":"87","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/contacts.test.ts":"88","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/messages.test.ts":"89","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/token.test.ts":"90","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/base64.test.ts":"91","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/buildHeaders.test.ts":"92","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/form.test.ts":"93","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/index.test.ts":"94","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/path.test.ts":"95","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/empty-keys-cases.ts":"96","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/stringify.test.ts":"97","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/utils.test.ts":"98","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/stringifyQuery.test.ts":"99","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/uploads.test.ts":"100"},{"size":618,"mtime":1758940895443,"results":"101","hashOfConfig":"102"},{"size":505,"mtime":1758940909627},{"size":373,"mtime":1758940909627},{"size":1195,"mtime":1758940895687,"results":"103","hashOfConfig":"104"},{"size":5794,"mtime":1758940909627},{"size":12713,"mtime":1758940895595,"results":"105","hashOfConfig":"104"},{"size":1472,"mtime":1758940909627},{"size":5655,"mtime":1758940895595,"results":"106","hashOfConfig":"104"},{"size":368,"mtime":1758940895595,"results":"107","hashOfConfig":"104"},{"size":804,"mtime":1758940909627},{"size":4165,"mtime":1758940909627},{"size":2884,"mtime":1758940895595,"results":"108","hashOfConfig":"104"},{"size":17071,"mtime":1758940895683,"results":"109","hashOfConfig":"104"},{"size":6588,"mtime":1758940909627},{"size":465,"mtime":1758940909627},{"size":932,"mtime":1758940909627},{"size":1553,"mtime":1758940909627},{"size":1169,"mtime":1758940909627},{"size":1326,"mtime":1758940909627},{"size":1171,"mtime":1758940909627},{"size":1101,"mtime":1758940909627},{"size":2927,"mtime":1758940909627},{"size":1586,"mtime":1758940909627},{"size":3551,"mtime":1758940909627},{"size":5680,"mtime":1758940909627},{"size":1416,"mtime":1758940909627},{"size":2205,"mtime":1758940909627},{"size":31,"mtime":1758940895595,"results":"110","hashOfConfig":"104"},{"size":6851,"mtime":1758940895599,"results":"111","hashOfConfig":"104"},{"size":29411,"mtime":1758940895599,"results":"112","hashOfConfig":"104"},{"size":16226,"mtime":1758940895603,"results":"113","hashOfConfig":"104"},{"size":6594,"mtime":1758940895603,"results":"114","hashOfConfig":"104"},{"size":3689,"mtime":1758940895447,"results":"115","hashOfConfig":"102"},{"size":92,"mtime":1758940909627},{"size":29685,"mtime":1758940909627},{"size":3148,"mtime":1758940909627},{"size":3989,"mtime":1758940909627},{"size":4462,"mtime":1758940909627},{"size":282,"mtime":1758940895679,"results":"116","hashOfConfig":"102"},{"size":119,"mtime":1758940895455,"results":"117","hashOfConfig":"102"},{"size":80,"mtime":1758940909627},{"size":641,"mtime":1758940909627},{"size":2917,"mtime":1758940909627},{"size":6407,"mtime":1758940909627},{"size":1187,"mtime":1758940909627},{"size":3026,"mtime":1758940909627},{"size":1512,"mtime":1758940909627},{"size":391,"mtime":1758940895475,"results":"118","hashOfConfig":"102"},{"size":325,"mtime":1758940895475,"results":"119","hashOfConfig":"102"},{"size":11734,"mtime":1758940895475,"results":"120","hashOfConfig":"102"},{"size":2183,"mtime":1758940895475,"results":"121","hashOfConfig":"102"},{"size":6748,"mtime":1758940909627},{"size":2473,"mtime":1758940909627},{"size":929,"mtime":1758940909631},{"size":3531,"mtime":1758940909631},{"size":5211,"mtime":1758940909631},{"size":6352,"mtime":1758940909631},{"size":6771,"mtime":1758940895479,"results":"122","hashOfConfig":"102"},{"size":1290,"mtime":1758940895667,"results":"123","hashOfConfig":"102"},{"size":831,"mtime":1758940895479,"results":"124","hashOfConfig":"102"},{"size":612,"mtime":1758940895667,"results":"125","hashOfConfig":"102"},{"size":3125,"mtime":1758940909631},{"size":3221,"mtime":1758940895483,"results":"126","hashOfConfig":"102"},{"size":182,"mtime":1758940895667,"results":"127","hashOfConfig":"102"},{"size":601,"mtime":1758940909631},{"size":3170,"mtime":1758940895667,"results":"128","hashOfConfig":"102"},{"size":271,"mtime":1758940895663,"results":"129","hashOfConfig":"102"},{"size":90,"mtime":1758940909631},{"size":86,"mtime":1758940909631},{"size":1468,"mtime":1758940909631},{"size":4444,"mtime":1758940909631},{"size":7748,"mtime":1758940909631},{"size":367,"mtime":1758940909631},{"size":2184,"mtime":1758940909631},{"size":119,"mtime":1758940895859,"results":"130","hashOfConfig":"102"},{"size":1158,"mtime":1758940909631},{"size":4185,"mtime":1758940909631},{"size":841,"mtime":1758940909631},{"size":4789,"mtime":1758940909631},{"size":1345,"mtime":1758940909631},{"size":35,"mtime":1758940895483,"results":"131","hashOfConfig":"102"},{"size":84,"mtime":1758940909631},{"size":59,"mtime":1758940909631},{"size":796,"mtime":1758940909631},{"size":2545,"mtime":1758940909631},{"size":3821,"mtime":1758940909631},{"size":1849,"mtime":1758940909631},{"size":1091,"mtime":1758940909631},{"size":2326,"mtime":1758940909631},{"size":790,"mtime":1758940909631},{"size":2073,"mtime":1758940895483,"results":"132","hashOfConfig":"104"},{"size":2193,"mtime":1758940895483,"results":"133","hashOfConfig":"104"},{"size":1910,"mtime":1758940895483,"results":"134","hashOfConfig":"104"},{"size":23587,"mtime":1758940909631},{"size":17601,"mtime":1758940895487,"results":"135","hashOfConfig":"104"},{"size":5918,"mtime":1758940895487,"results":"136","hashOfConfig":"104"},{"size":81651,"mtime":1758940895495,"results":"137","hashOfConfig":"104"},{"size":6180,"mtime":1758940909631},{"size":743,"mtime":1758940909631},{"size":3507,"mtime":1758940895495,"results":"138","hashOfConfig":"104"},{"filePath":"139","messages":"140","suppressedMessages":"141","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"19ztag9",{"filePath":"142","messages":"143","suppressedMessages":"144","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"xx1ffk",{"filePath":"145","messages":"146","suppressedMessages":"147","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"148","messages":"149","suppressedMessages":"150","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"151","messages":"152","suppressedMessages":"153","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"154","messages":"155","suppressedMessages":"156","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"157","messages":"158","suppressedMessages":"159","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"160","messages":"161","suppressedMessages":"162","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"163","messages":"164","suppressedMessages":"165","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"166","messages":"167","suppressedMessages":"168","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"169","messages":"170","suppressedMessages":"171","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"172","messages":"173","suppressedMessages":"174","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"175","messages":"176","suppressedMessages":"177","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"178","messages":"179","suppressedMessages":"180","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"181","messages":"182","suppressedMessages":"183","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"184","messages":"185","suppressedMessages":"186","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"187","messages":"188","suppressedMessages":"189","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"190","messages":"191","suppressedMessages":"192","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"193","messages":"194","suppressedMessages":"195","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"196","messages":"197","suppressedMessages":"198","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"199","messages":"200","suppressedMessages":"201","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"202","messages":"203","suppressedMessages":"204","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"205","messages":"206","suppressedMessages":"207","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"208","messages":"209","suppressedMessages":"210","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"211","messages":"212","suppressedMessages":"213","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"214","messages":"215","suppressedMessages":"216","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"217","messages":"218","suppressedMessages":"219","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"220","messages":"221","suppressedMessages":"222","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"223","messages":"224","suppressedMessages":"225","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"226","messages":"227","suppressedMessages":"228","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"229","messages":"230","suppressedMessages":"231","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"232","messages":"233","suppressedMessages":"234","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"235","messages":"236","suppressedMessages":"237","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"238","messages":"239","suppressedMessages":"240","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"241","messages":"242","suppressedMessages":"243","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"244","messages":"245","suppressedMessages":"246","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/jest.config.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/code-tool-worker.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/compat.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/dynamic-tools.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/filtering.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/index.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/options.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/dynamic-tools.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/compat.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/options.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/tools.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/scripts/publish-packages.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/resource.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/uploads.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/formats.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/index.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/stringify.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/types.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/uploads.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/base64.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/bytes.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/env.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/path.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/sleep.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/values.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/base64.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/buildHeaders.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/form.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/path.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/empty-keys-cases.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/stringify.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/uploads.test.ts",[],[]] \ No newline at end of file diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index eef52aa..6dc52c3 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -19,3 +19,4 @@ jobs: bash ./bin/check-release-environment env: NPM_TOKEN: ${{ secrets.BEEPER_DESKTOP_NPM_TOKEN || secrets.NPM_TOKEN }} + diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts index 15ce7f5..3d6655a 100644 --- a/packages/mcp-server/src/code-tool-paths.cts +++ b/packages/mcp-server/src/code-tool-paths.cts @@ -1,3 +1,3 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export const workerPath = require.resolve('./code-tool-worker.mjs'); +export const workerPath = require.resolve('./code-tool-worker.mjs') From 22d7f29cd477bf4d22c68b51cd4b99bcf710efbd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 02:44:15 +0000 Subject: [PATCH 09/36] chore(internal): ignore .eslintcache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 74cba89..d62bea5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ dist dist-deno /*.tgz .idea/ +.eslintcache dist-bundle *.mcpb From 98c6489b8780dbe7d4ba377fb64364fb862fc7f7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 02:29:42 +0000 Subject: [PATCH 10/36] fix(mcp): fix cli argument parsing logic --- .eslintcache | 2 +- packages/mcp-server/src/options.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.eslintcache b/.eslintcache index 2798f1b..d56f11a 100644 --- a/.eslintcache +++ b/.eslintcache @@ -1 +1 @@ -[{"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/jest.config.ts":"1","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/jest.config.ts":"2","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/code-tool-types.ts":"3","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/code-tool-worker.ts":"4","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/code-tool.ts":"5","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/compat.ts":"6","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/docs-search-tool.ts":"7","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/dynamic-tools.ts":"8","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/filtering.ts":"9","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/headers.ts":"10","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/http.ts":"11","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/index.ts":"12","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/options.ts":"13","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/server.ts":"14","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/stdio.ts":"15","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/accounts/get-accounts.ts":"16","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/app/open-in-app.ts":"17","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/app/search.ts":"18","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/get-chat.ts":"19","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/archive-chat.ts":"20","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts":"21","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/index.ts":"22","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts":"23","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/chats/search-chats.ts":"24","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/messages/search-messages.ts":"25","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/messages/send-message.ts":"26","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools/types.ts":"27","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools.ts":"28","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/dynamic-tools.test.ts":"29","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/compat.test.ts":"30","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/options.test.ts":"31","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/tools.test.ts":"32","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/scripts/publish-packages.ts":"33","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/api-promise.ts":"34","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/client.ts":"35","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/api-promise.ts":"36","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/error.ts":"37","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/pagination.ts":"38","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/resource.ts":"39","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/uploads.ts":"40","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/error.ts":"41","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/index.ts":"42","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/builtin-types.ts":"43","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/detect-platform.ts":"44","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/errors.ts":"45","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/headers.ts":"46","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/parse.ts":"47","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/formats.ts":"48","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/index.ts":"49","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/stringify.ts":"50","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/types.ts":"51","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/utils.ts":"52","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/request-options.ts":"53","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/shim-types.ts":"54","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/shims.ts":"55","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/to-file.ts":"56","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/types.ts":"57","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/uploads.ts":"58","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/base64.ts":"59","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/bytes.ts":"60","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/env.ts":"61","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/log.ts":"62","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/path.ts":"63","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/sleep.ts":"64","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/uuid.ts":"65","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/values.ts":"66","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils.ts":"67","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/pagination.ts":"68","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resource.ts":"69","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/accounts.ts":"70","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/app.ts":"71","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats/chats.ts":"72","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats/index.ts":"73","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats/reminders.ts":"74","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats.ts":"75","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/contacts.ts":"76","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/messages.ts":"77","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/index.ts":"78","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/shared.ts":"79","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/token.ts":"80","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources.ts":"81","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/uploads.ts":"82","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/version.ts":"83","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/accounts.test.ts":"84","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/app.test.ts":"85","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/chats/chats.test.ts":"86","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/chats/reminders.test.ts":"87","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/contacts.test.ts":"88","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/messages.test.ts":"89","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/api-resources/token.test.ts":"90","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/base64.test.ts":"91","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/buildHeaders.test.ts":"92","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/form.test.ts":"93","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/index.test.ts":"94","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/path.test.ts":"95","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/empty-keys-cases.ts":"96","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/stringify.test.ts":"97","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/utils.test.ts":"98","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/stringifyQuery.test.ts":"99","/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/uploads.test.ts":"100"},{"size":618,"mtime":1758940895443,"results":"101","hashOfConfig":"102"},{"size":505,"mtime":1758940909627},{"size":373,"mtime":1758940909627},{"size":1195,"mtime":1758940895687,"results":"103","hashOfConfig":"104"},{"size":5794,"mtime":1758940909627},{"size":12713,"mtime":1758940895595,"results":"105","hashOfConfig":"104"},{"size":1472,"mtime":1758940909627},{"size":5655,"mtime":1758940895595,"results":"106","hashOfConfig":"104"},{"size":368,"mtime":1758940895595,"results":"107","hashOfConfig":"104"},{"size":804,"mtime":1758940909627},{"size":4165,"mtime":1758940909627},{"size":2884,"mtime":1758940895595,"results":"108","hashOfConfig":"104"},{"size":17071,"mtime":1758940895683,"results":"109","hashOfConfig":"104"},{"size":6588,"mtime":1758940909627},{"size":465,"mtime":1758940909627},{"size":932,"mtime":1758940909627},{"size":1553,"mtime":1758940909627},{"size":1169,"mtime":1758940909627},{"size":1326,"mtime":1758940909627},{"size":1171,"mtime":1758940909627},{"size":1101,"mtime":1758940909627},{"size":2927,"mtime":1758940909627},{"size":1586,"mtime":1758940909627},{"size":3551,"mtime":1758940909627},{"size":5680,"mtime":1758940909627},{"size":1416,"mtime":1758940909627},{"size":2205,"mtime":1758940909627},{"size":31,"mtime":1758940895595,"results":"110","hashOfConfig":"104"},{"size":6851,"mtime":1758940895599,"results":"111","hashOfConfig":"104"},{"size":29411,"mtime":1758940895599,"results":"112","hashOfConfig":"104"},{"size":16226,"mtime":1758940895603,"results":"113","hashOfConfig":"104"},{"size":6594,"mtime":1758940895603,"results":"114","hashOfConfig":"104"},{"size":3689,"mtime":1758940895447,"results":"115","hashOfConfig":"102"},{"size":92,"mtime":1758940909627},{"size":29685,"mtime":1758940909627},{"size":3148,"mtime":1758940909627},{"size":3989,"mtime":1758940909627},{"size":4462,"mtime":1758940909627},{"size":282,"mtime":1758940895679,"results":"116","hashOfConfig":"102"},{"size":119,"mtime":1758940895455,"results":"117","hashOfConfig":"102"},{"size":80,"mtime":1758940909627},{"size":641,"mtime":1758940909627},{"size":2917,"mtime":1758940909627},{"size":6407,"mtime":1758940909627},{"size":1187,"mtime":1758940909627},{"size":3026,"mtime":1758940909627},{"size":1512,"mtime":1758940909627},{"size":391,"mtime":1758940895475,"results":"118","hashOfConfig":"102"},{"size":325,"mtime":1758940895475,"results":"119","hashOfConfig":"102"},{"size":11734,"mtime":1758940895475,"results":"120","hashOfConfig":"102"},{"size":2183,"mtime":1758940895475,"results":"121","hashOfConfig":"102"},{"size":6748,"mtime":1758940909627},{"size":2473,"mtime":1758940909627},{"size":929,"mtime":1758940909631},{"size":3531,"mtime":1758940909631},{"size":5211,"mtime":1758940909631},{"size":6352,"mtime":1758940909631},{"size":6771,"mtime":1758940895479,"results":"122","hashOfConfig":"102"},{"size":1290,"mtime":1758940895667,"results":"123","hashOfConfig":"102"},{"size":831,"mtime":1758940895479,"results":"124","hashOfConfig":"102"},{"size":612,"mtime":1758940895667,"results":"125","hashOfConfig":"102"},{"size":3125,"mtime":1758940909631},{"size":3221,"mtime":1758940895483,"results":"126","hashOfConfig":"102"},{"size":182,"mtime":1758940895667,"results":"127","hashOfConfig":"102"},{"size":601,"mtime":1758940909631},{"size":3170,"mtime":1758940895667,"results":"128","hashOfConfig":"102"},{"size":271,"mtime":1758940895663,"results":"129","hashOfConfig":"102"},{"size":90,"mtime":1758940909631},{"size":86,"mtime":1758940909631},{"size":1468,"mtime":1758940909631},{"size":4444,"mtime":1758940909631},{"size":7748,"mtime":1758940909631},{"size":367,"mtime":1758940909631},{"size":2184,"mtime":1758940909631},{"size":119,"mtime":1758940895859,"results":"130","hashOfConfig":"102"},{"size":1158,"mtime":1758940909631},{"size":4185,"mtime":1758940909631},{"size":841,"mtime":1758940909631},{"size":4789,"mtime":1758940909631},{"size":1345,"mtime":1758940909631},{"size":35,"mtime":1758940895483,"results":"131","hashOfConfig":"102"},{"size":84,"mtime":1758940909631},{"size":59,"mtime":1758940909631},{"size":796,"mtime":1758940909631},{"size":2545,"mtime":1758940909631},{"size":3821,"mtime":1758940909631},{"size":1849,"mtime":1758940909631},{"size":1091,"mtime":1758940909631},{"size":2326,"mtime":1758940909631},{"size":790,"mtime":1758940909631},{"size":2073,"mtime":1758940895483,"results":"132","hashOfConfig":"104"},{"size":2193,"mtime":1758940895483,"results":"133","hashOfConfig":"104"},{"size":1910,"mtime":1758940895483,"results":"134","hashOfConfig":"104"},{"size":23587,"mtime":1758940909631},{"size":17601,"mtime":1758940895487,"results":"135","hashOfConfig":"104"},{"size":5918,"mtime":1758940895487,"results":"136","hashOfConfig":"104"},{"size":81651,"mtime":1758940895495,"results":"137","hashOfConfig":"104"},{"size":6180,"mtime":1758940909631},{"size":743,"mtime":1758940909631},{"size":3507,"mtime":1758940895495,"results":"138","hashOfConfig":"104"},{"filePath":"139","messages":"140","suppressedMessages":"141","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"19ztag9",{"filePath":"142","messages":"143","suppressedMessages":"144","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"xx1ffk",{"filePath":"145","messages":"146","suppressedMessages":"147","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"148","messages":"149","suppressedMessages":"150","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"151","messages":"152","suppressedMessages":"153","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"154","messages":"155","suppressedMessages":"156","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"157","messages":"158","suppressedMessages":"159","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"160","messages":"161","suppressedMessages":"162","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"163","messages":"164","suppressedMessages":"165","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"166","messages":"167","suppressedMessages":"168","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"169","messages":"170","suppressedMessages":"171","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"172","messages":"173","suppressedMessages":"174","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"175","messages":"176","suppressedMessages":"177","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"178","messages":"179","suppressedMessages":"180","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"181","messages":"182","suppressedMessages":"183","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"184","messages":"185","suppressedMessages":"186","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"187","messages":"188","suppressedMessages":"189","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"190","messages":"191","suppressedMessages":"192","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"193","messages":"194","suppressedMessages":"195","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"196","messages":"197","suppressedMessages":"198","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"199","messages":"200","suppressedMessages":"201","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"202","messages":"203","suppressedMessages":"204","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"205","messages":"206","suppressedMessages":"207","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"208","messages":"209","suppressedMessages":"210","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"211","messages":"212","suppressedMessages":"213","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"214","messages":"215","suppressedMessages":"216","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"217","messages":"218","suppressedMessages":"219","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"220","messages":"221","suppressedMessages":"222","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"223","messages":"224","suppressedMessages":"225","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"226","messages":"227","suppressedMessages":"228","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"229","messages":"230","suppressedMessages":"231","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"232","messages":"233","suppressedMessages":"234","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"235","messages":"236","suppressedMessages":"237","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"238","messages":"239","suppressedMessages":"240","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"241","messages":"242","suppressedMessages":"243","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"244","messages":"245","suppressedMessages":"246","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/jest.config.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/code-tool-worker.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/compat.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/dynamic-tools.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/filtering.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/index.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/options.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/tools.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/dynamic-tools.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/compat.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/options.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/tests/tools.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/scripts/publish-packages.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/resource.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/core/uploads.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/formats.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/index.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/stringify.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/qs/types.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/uploads.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/base64.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/bytes.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/env.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/path.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/sleep.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils/values.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/internal/utils.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources/chats.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/src/resources.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/base64.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/buildHeaders.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/form.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/path.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/empty-keys-cases.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/qs/stringify.test.ts",[],[],"/home/tempuser-u5wygz/run/codegen-output/beeper/beeper-desktop-api-typescript/tests/uploads.test.ts",[],[]] \ No newline at end of file +[{"/home/tempuser-knb06p/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/options.ts":"1"},{"size":17015,"mtime":1759199378805,"results":"2","hashOfConfig":"3"},{"filePath":"4","messages":"5","suppressedMessages":"6","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"xx1ffk","/home/tempuser-knb06p/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/options.ts",[],[]] \ No newline at end of file diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 348a33d..4fe3b98 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -247,9 +247,10 @@ export function parseCLIOptions(): CLIOptions { } const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code' | 'docs') => - explicitTools ? argv.tools?.includes(toolType) && !argv.noTools?.includes(toolType) : undefined; + argv.noTools?.includes(toolType) ? false + : argv.tools?.includes(toolType) ? true + : undefined; - const explicitTools = Boolean(argv.tools || argv.noTools); const includeDynamicTools = shouldIncludeToolType('dynamic'); const includeAllTools = shouldIncludeToolType('all'); const includeCodeTools = shouldIncludeToolType('code'); From 2c8444166c9b4e6f67c9bf4827989aee65d672e8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 02:32:28 +0000 Subject: [PATCH 11/36] fix(mcp): resolve a linting issue in server code --- packages/mcp-server/src/code-tool-paths.cts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts index 3d6655a..15ce7f5 100644 --- a/packages/mcp-server/src/code-tool-paths.cts +++ b/packages/mcp-server/src/code-tool-paths.cts @@ -1,3 +1,3 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export const workerPath = require.resolve('./code-tool-worker.mjs') +export const workerPath = require.resolve('./code-tool-worker.mjs'); From a9a736fba58dde3c39bc2f5501be1ae30f380367 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 02:32:59 +0000 Subject: [PATCH 12/36] chore: update lockfile --- packages/mcp-server/yarn.lock | 306 +--------------------------------- 1 file changed, 4 insertions(+), 302 deletions(-) diff --git a/packages/mcp-server/yarn.lock b/packages/mcp-server/yarn.lock index ad81983..966d057 100644 --- a/packages/mcp-server/yarn.lock +++ b/packages/mcp-server/yarn.lock @@ -10,20 +10,6 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@anthropic-ai/dxt@^0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@anthropic-ai/dxt/-/dxt-0.2.6.tgz#636197c3d083c9136ac3b5a11d2ba82477fdc2c6" - integrity sha512-5VSqKRpkytTYh5UJz9jOaI8zLXNCe4Gc+ArKGFV6IeWnEPP0Qnd0k+V3pO8cYzp92Puf/+Cgo0xc4haE0azTXg== - dependencies: - "@inquirer/prompts" "^6.0.1" - commander "^13.1.0" - fflate "^0.8.2" - galactus "^1.0.0" - ignore "^7.0.5" - node-forge "^1.3.1" - pretty-bytes "^5.6.0" - zod "^3.25.67" - "@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" @@ -350,144 +336,6 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@inquirer/checkbox@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-3.0.1.tgz#0a57f704265f78c36e17f07e421b98efb4b9867b" - integrity sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/figures" "^1.0.6" - "@inquirer/type" "^2.0.0" - ansi-escapes "^4.3.2" - yoctocolors-cjs "^2.1.2" - -"@inquirer/confirm@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-4.0.1.tgz#9106d6bffa0b2fdd0e4f60319b6f04f2e06e6e25" - integrity sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/type" "^2.0.0" - -"@inquirer/core@^9.2.1": - version "9.2.1" - resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-9.2.1.tgz#677c49dee399c9063f31e0c93f0f37bddc67add1" - integrity sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg== - dependencies: - "@inquirer/figures" "^1.0.6" - "@inquirer/type" "^2.0.0" - "@types/mute-stream" "^0.0.4" - "@types/node" "^22.5.5" - "@types/wrap-ansi" "^3.0.0" - ansi-escapes "^4.3.2" - cli-width "^4.1.0" - mute-stream "^1.0.0" - signal-exit "^4.1.0" - strip-ansi "^6.0.1" - wrap-ansi "^6.2.0" - yoctocolors-cjs "^2.1.2" - -"@inquirer/editor@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-3.0.1.tgz#d109f21e050af6b960725388cb1c04214ed7c7bc" - integrity sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/type" "^2.0.0" - external-editor "^3.1.0" - -"@inquirer/expand@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-3.0.1.tgz#aed9183cac4d12811be47a4a895ea8e82a17e22c" - integrity sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/type" "^2.0.0" - yoctocolors-cjs "^2.1.2" - -"@inquirer/figures@^1.0.6": - version "1.0.13" - resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.13.tgz#ad0afd62baab1c23175115a9b62f511b6a751e45" - integrity sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw== - -"@inquirer/input@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-3.0.1.tgz#de63d49e516487388508d42049deb70f2cb5f28e" - integrity sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/type" "^2.0.0" - -"@inquirer/number@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-2.0.1.tgz#b9863080d02ab7dc2e56e16433d83abea0f2a980" - integrity sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/type" "^2.0.0" - -"@inquirer/password@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-3.0.1.tgz#2a9a9143591088336bbd573bcb05d5bf080dbf87" - integrity sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/type" "^2.0.0" - ansi-escapes "^4.3.2" - -"@inquirer/prompts@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-6.0.1.tgz#43f5c0ed35c5ebfe52f1d43d46da2d363d950071" - integrity sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A== - dependencies: - "@inquirer/checkbox" "^3.0.1" - "@inquirer/confirm" "^4.0.1" - "@inquirer/editor" "^3.0.1" - "@inquirer/expand" "^3.0.1" - "@inquirer/input" "^3.0.1" - "@inquirer/number" "^2.0.1" - "@inquirer/password" "^3.0.1" - "@inquirer/rawlist" "^3.0.1" - "@inquirer/search" "^2.0.1" - "@inquirer/select" "^3.0.1" - -"@inquirer/rawlist@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-3.0.1.tgz#729def358419cc929045f264131878ed379e0af3" - integrity sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/type" "^2.0.0" - yoctocolors-cjs "^2.1.2" - -"@inquirer/search@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-2.0.1.tgz#69b774a0a826de2e27b48981d01bc5ad81e73721" - integrity sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/figures" "^1.0.6" - "@inquirer/type" "^2.0.0" - yoctocolors-cjs "^2.1.2" - -"@inquirer/select@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-3.0.1.tgz#1df9ed27fb85a5f526d559ac5ce7cc4e9dc4e7ec" - integrity sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q== - dependencies: - "@inquirer/core" "^9.2.1" - "@inquirer/figures" "^1.0.6" - "@inquirer/type" "^2.0.0" - ansi-escapes "^4.3.2" - yoctocolors-cjs "^2.1.2" - -"@inquirer/type@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-2.0.0.tgz#08fa513dca2cb6264fe1b0a2fabade051444e3f6" - integrity sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag== - dependencies: - mute-stream "^1.0.0" - "@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" @@ -947,13 +795,6 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== -"@types/mute-stream@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@types/mute-stream/-/mute-stream-0.0.4.tgz#77208e56a08767af6c5e1237be8888e2f255c478" - integrity sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow== - dependencies: - "@types/node" "*" - "@types/node@*": version "22.15.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.17.tgz#355ccec95f705b664e4332bb64a7f07db30b7055" @@ -961,13 +802,6 @@ dependencies: undici-types "~6.21.0" -"@types/node@^22.5.5": - version "22.18.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.18.0.tgz#9e4709be4f104e3568f7dd1c71e2949bf147a47b" - integrity sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ== - dependencies: - undici-types "~6.21.0" - "@types/qs@*", "@types/qs@^6.14.0": version "6.14.0" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" @@ -1000,11 +834,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== -"@types/wrap-ansi@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" - integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== - "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -1151,7 +980,7 @@ ajv@^6.12.4, ajv@^6.12.6: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-escapes@^4.2.1, ansi-escapes@^4.3.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== @@ -1393,11 +1222,6 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - ci-info@^3.2.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" @@ -1413,11 +1237,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-width@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" - integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -1454,11 +1273,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -commander@^13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-13.1.0.tgz#776167db68c78f38dcce1f9b8d7b8b9a488abf46" - integrity sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1871,15 +1685,6 @@ express@^5.0.1, express@^5.1.0: type-is "^2.0.1" vary "^1.1.2" -external-editor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - 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" @@ -1925,11 +1730,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fflate@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" - integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== - 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" @@ -1993,14 +1793,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== -flora-colossus@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/flora-colossus/-/flora-colossus-2.0.0.tgz#af1e85db0a8256ef05f3fb531c1235236c97220a" - integrity sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA== - dependencies: - debug "^4.3.4" - fs-extra "^10.1.0" - forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -2011,15 +1803,6 @@ fresh@^2.0.0: resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== -fs-extra@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2035,15 +1818,6 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -galactus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/galactus/-/galactus-1.0.0.tgz#c2615182afa0c6d0859b92e56ae36d052827db7e" - integrity sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ== - dependencies: - debug "^4.3.4" - flora-colossus "^2.0.0" - fs-extra "^10.1.0" - 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" @@ -2136,7 +1910,7 @@ gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: +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== @@ -2191,23 +1965,11 @@ iconv-lite@0.6.3, iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - 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== -ignore@^7.0.5: - version "7.0.5" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" - integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== - import-fresh@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" @@ -2786,15 +2548,6 @@ json5@^2.2.2, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^6.0.1: - version "6.2.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" - integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -2968,11 +2721,6 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -mute-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" - integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2983,11 +2731,6 @@ negotiator@^1.0.0: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== -node-forge@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -3053,11 +2796,6 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - p-all@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-all/-/p-all-3.0.0.tgz#077c023c37e75e760193badab2bad3ccd5782bfb" @@ -3201,11 +2939,6 @@ prettier@^3.0.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5" integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw== -pretty-bytes@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" - integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== - 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" @@ -3353,7 +3086,7 @@ safe-buffer@5.2.1, safe-buffer@~5.2.0: 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", "safer-buffer@>= 2.1.2 < 3.0.0": +"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== @@ -3457,11 +3190,6 @@ signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -3606,13 +3334,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -3753,11 +3474,6 @@ undici-types@~6.21.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -3821,15 +3537,6 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -3890,11 +3597,6 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yoctocolors-cjs@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz#7e4964ea8ec422b7a40ac917d3a344cfd2304baa" - integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== - 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" @@ -3910,7 +3612,7 @@ zod@^3.23.8: resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.4.tgz#e2e2cca5faaa012d76e527d0d36622e0a90c315f" integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg== -zod@^3.25.20, zod@^3.25.67: +zod@^3.25.20: version "3.25.76" resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== From f2a0dfbd7930292f715ab530a6dedbf53e6ae8e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 02:55:10 +0000 Subject: [PATCH 13/36] chore(internal): remove .eslintcache --- .eslintcache | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .eslintcache diff --git a/.eslintcache b/.eslintcache deleted file mode 100644 index d56f11a..0000000 --- a/.eslintcache +++ /dev/null @@ -1 +0,0 @@ -[{"/home/tempuser-knb06p/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/options.ts":"1"},{"size":17015,"mtime":1759199378805,"results":"2","hashOfConfig":"3"},{"filePath":"4","messages":"5","suppressedMessages":"6","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"xx1ffk","/home/tempuser-knb06p/run/codegen-output/beeper/beeper-desktop-api-typescript/packages/mcp-server/src/options.ts",[],[]] \ No newline at end of file From 3598172f0839f111c6000c9f29502d21afabee0c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 02:55:08 +0000 Subject: [PATCH 14/36] chore(internal): codegen related update --- src/client.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/client.ts b/src/client.ts index d9bc8cf..492b916 100644 --- a/src/client.ts +++ b/src/client.ts @@ -246,17 +246,9 @@ export class BeeperDesktop { } protected async authHeaders(opts: FinalRequestOptions): Promise { - return buildHeaders([await this.bearerAuth(opts), await this.oauth2Auth(opts)]); - } - - protected async bearerAuth(opts: FinalRequestOptions): Promise { return buildHeaders([{ Authorization: `Bearer ${this.accessToken}` }]); } - protected async oauth2Auth(opts: FinalRequestOptions): Promise { - return undefined; - } - protected stringifyQuery(query: Record): string { return qs.stringify(query, { arrayFormat: 'repeat' }); } From 8cf89a9e31969a45d24179342955b2a4e931ae6f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 02:18:47 +0000 Subject: [PATCH 15/36] =?UTF-8?q?chore(jsdoc):=20fix=20@link=20annotations?= =?UTF-8?q?=20to=20refer=20only=20to=20parts=20of=20the=20package=E2=80=98?= =?UTF-8?q?s=20public=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/internal/to-file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/to-file.ts b/src/internal/to-file.ts index 245e849..30eada3 100644 --- a/src/internal/to-file.ts +++ b/src/internal/to-file.ts @@ -73,7 +73,7 @@ export type ToFileInput = /** * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats - * @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s + * @param value the raw content of the file. Can be an {@link Uploadable}, BlobLikePart, or AsyncIterable of BlobLikeParts * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible * @param {Object=} options additional properties * @param {string=} options.type the MIME type of the content From 31e0341efbad7c4b9abc13d145c477eb3676cc0b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 02:39:31 +0000 Subject: [PATCH 16/36] chore(internal): use npm pack for build uploads --- scripts/utils/upload-artifact.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 35e2b8f..826f861 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -12,9 +12,11 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(tar "${BASE_PATH:+-C$BASE_PATH}" -cz "${ARTIFACT_PATH:-dist}" | curl -v -X PUT \ +TARBALL=$(cd dist && npm pack --silent) + +UPLOAD_RESPONSE=$(curl -v -X PUT \ -H "Content-Type: application/gzip" \ - --data-binary @- "$SIGNED_URL" 2>&1) + --data-binary "@dist/$TARBALL" "$SIGNED_URL" 2>&1) if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" From d1a5970f917e9ddfe1060a4129feee7bb1be1414 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:48:54 +0000 Subject: [PATCH 17/36] feat(api): manual updates --- .stats.yml | 2 +- api.md | 28 +-- packages/mcp-server/README.md | 14 +- packages/mcp-server/src/tools/index.ts | 6 +- .../tools/{app => top-level}/open-in-app.ts | 4 +- .../src/tools/{app => top-level}/search.ts | 4 +- src/client.ts | 86 ++++++--- src/resources/app.ts | 178 ------------------ src/resources/index.ts | 17 +- src/resources/top-level.ts | 125 ++++++++++++ .../{app.test.ts => top-level.test.ts} | 14 +- 11 files changed, 232 insertions(+), 246 deletions(-) rename packages/mcp-server/src/tools/{app => top-level}/open-in-app.ts (95%) rename packages/mcp-server/src/tools/{app => top-level}/search.ts (93%) delete mode 100644 src/resources/app.ts create mode 100644 src/resources/top-level.ts rename tests/api-resources/{app.test.ts => top-level.test.ts} (86%) diff --git a/.stats.yml b/.stats.yml index fc8ce6a..85ae457 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 -config_hash: 061b75b88f80bb43b4121e5e7c1255e2 +config_hash: e404c86d86b33ff749bded2ba4d2c725 diff --git a/api.md b/api.md index 54b94fd..8b6120c 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,17 @@ +# BeeperDesktop + +Types: + +- DownloadAssetResponse +- OpenResponse +- SearchResponse + +Methods: + +- client.downloadAsset({ ...params }) -> DownloadAssetResponse +- client.open({ ...params }) -> OpenResponse +- client.search({ ...params }) -> SearchResponse + # Shared Types: @@ -20,20 +34,6 @@ Methods: - client.accounts.list() -> AccountListResponse -# App - -Types: - -- AppDownloadAssetResponse -- AppOpenResponse -- AppSearchResponse - -Methods: - -- client.app.downloadAsset({ ...params }) -> AppDownloadAssetResponse -- client.app.open({ ...params }) -> AppOpenResponse -- client.app.search({ ...params }) -> AppSearchResponse - # Contacts Types: diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 2ab4e18..a22ced9 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -180,7 +180,7 @@ http://localhost:3000?client=cursor&capability=tool-name-length%3D40 import { server, endpoints, init } from "@beeper/desktop-mcp/server"; // import a specific tool -import getAccounts from "@beeper/desktop-mcp/tools/accounts/get-accounts"; +import openInApp from "@beeper/desktop-mcp/tools/top-level/open-in-app"; // initialize the server and all endpoints init({ server, endpoints }); @@ -205,22 +205,22 @@ const myCustomEndpoint = { }; // initialize the server with your custom endpoints -init({ server: myServer, endpoints: [getAccounts, myCustomEndpoint] }); +init({ server: myServer, endpoints: [openInApp, myCustomEndpoint] }); ``` ## Available Tools The following tools are available in this MCP server. -### Resource `accounts`: - -- `get_accounts` (`read`) tags: [accounts]: List connected accounts on this device. Use to pick account context. - -### Resource `app`: +### Resource `$client`: - `open_in_app` (`write`) tags: [app]: Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment. - `search` (`read`) tags: [app]: Search for chats, participant name matches in groups, and the first page of messages in one call. Use this when the user asks for a specific chat, group, or person. +### Resource `accounts`: + +- `get_accounts` (`read`) tags: [accounts]: List connected accounts on this device. Use to pick account context. + ### Resource `chats`: - `get_chat` (`read`) tags: [chats]: Get chat details: metadata, participants (limited), last activity. diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index e3ad0a9..4f1144e 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -4,9 +4,9 @@ import { Metadata, Endpoint, HandlerFunction } from './types'; export { Metadata, Endpoint, HandlerFunction }; +import open_in_app from './top-level/open-in-app'; +import search from './top-level/search'; import get_accounts from './accounts/get-accounts'; -import open_in_app from './app/open-in-app'; -import search from './app/search'; import get_chat from './chats/get-chat'; import archive_chat from './chats/archive-chat'; import search_chats from './chats/search-chats'; @@ -21,9 +21,9 @@ function addEndpoint(endpoint: Endpoint) { endpoints.push(endpoint); } -addEndpoint(get_accounts); addEndpoint(open_in_app); addEndpoint(search); +addEndpoint(get_accounts); addEndpoint(get_chat); addEndpoint(archive_chat); addEndpoint(search_chats); diff --git a/packages/mcp-server/src/tools/app/open-in-app.ts b/packages/mcp-server/src/tools/top-level/open-in-app.ts similarity index 95% rename from packages/mcp-server/src/tools/app/open-in-app.ts rename to packages/mcp-server/src/tools/top-level/open-in-app.ts index bf8fbb1..a7c6529 100644 --- a/packages/mcp-server/src/tools/app/open-in-app.ts +++ b/packages/mcp-server/src/tools/top-level/open-in-app.ts @@ -6,7 +6,7 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; export const metadata: Metadata = { - resource: 'app', + resource: '$client', operation: 'write', tags: ['app'], httpMethod: 'post', @@ -46,7 +46,7 @@ export const tool: Tool = { export const handler = async (client: BeeperDesktop, args: Record | undefined) => { const body = args as any; - return asTextContentResult(await client.app.open(body)); + return asTextContentResult(await client.open(body)); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/app/search.ts b/packages/mcp-server/src/tools/top-level/search.ts similarity index 93% rename from packages/mcp-server/src/tools/app/search.ts rename to packages/mcp-server/src/tools/top-level/search.ts index a911592..5ae68e8 100644 --- a/packages/mcp-server/src/tools/app/search.ts +++ b/packages/mcp-server/src/tools/top-level/search.ts @@ -6,7 +6,7 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; export const metadata: Metadata = { - resource: 'app', + resource: '$client', operation: 'read', tags: ['app'], httpMethod: 'get', @@ -35,7 +35,7 @@ export const tool: Tool = { export const handler = async (client: BeeperDesktop, args: Record | undefined) => { const body = args as any; - return asTextContentResult(await client.app.search(body)); + return asTextContentResult(await client.search(body)); }; export default { metadata, tool, handler }; diff --git a/src/client.ts b/src/client.ts index 492b916..135d166 100644 --- a/src/client.ts +++ b/src/client.ts @@ -18,17 +18,17 @@ import * as Pagination from './core/pagination'; import { AbstractPage, type CursorParams, CursorResponse } from './core/pagination'; import * as Uploads from './core/uploads'; import * as API from './resources/index'; +import * as TopLevelAPI from './resources/top-level'; +import { + DownloadAssetParams, + DownloadAssetResponse, + OpenParams, + OpenResponse, + SearchParams, + SearchResponse, +} from './resources/top-level'; import { APIPromise } from './core/api-promise'; import { Account, AccountListResponse, Accounts } from './resources/accounts'; -import { - App, - AppDownloadAssetParams, - AppDownloadAssetResponse, - AppOpenParams, - AppOpenResponse, - AppSearchParams, - AppSearchResponse, -} from './resources/app'; import { ContactSearchParams, ContactSearchResponse, Contacts } from './resources/contacts'; import { MessageSearchParams, MessageSendParams, MessageSendResponse, Messages } from './resources/messages'; import { RevokeRequest, Token, UserInfo } from './resources/token'; @@ -237,6 +237,52 @@ export class BeeperDesktop { return this.baseURL !== 'http://localhost:23373'; } + /** + * Download a Matrix asset using its mxc:// or localmxc:// URL and return the local + * file URL. + * + * @example + * ```ts + * const response = await client.downloadAsset({ url: 'x' }); + * ``` + */ + downloadAsset( + body: TopLevelAPI.DownloadAssetParams, + options?: RequestOptions, + ): APIPromise { + return this.post('/v0/download-asset', { body, ...options }); + } + + /** + * Open Beeper Desktop and optionally navigate to a specific chat, message, or + * pre-fill draft text and attachment. + * + * @example + * ```ts + * const response = await client.open(); + * ``` + */ + open( + body: TopLevelAPI.OpenParams | null | undefined = {}, + options?: RequestOptions, + ): APIPromise { + return this.post('/v0/open-app', { body, ...options }); + } + + /** + * Returns matching chats, participant name matches in groups, and the first page + * of messages in one call. Paginate messages via search-messages. Paginate chats + * via search-chats. Uses the same sorting as the chat search in the app. + * + * @example + * ```ts + * const response = await client.search({ query: 'x' }); + * ``` + */ + search(query: TopLevelAPI.SearchParams, options?: RequestOptions): APIPromise { + return this.get('/v0/search', { query, ...options }); + } + protected defaultQuery(): Record | undefined { return this._options.defaultQuery; } @@ -760,10 +806,6 @@ export class BeeperDesktop { * Accounts operations */ accounts: API.Accounts = new API.Accounts(this); - /** - * App operations - */ - app: API.App = new API.App(this); /** * Contacts operations */ @@ -783,7 +825,6 @@ export class BeeperDesktop { } BeeperDesktop.Accounts = Accounts; -BeeperDesktop.App = App; BeeperDesktop.Contacts = Contacts; BeeperDesktop.Chats = Chats; BeeperDesktop.Messages = Messages; @@ -795,18 +836,17 @@ export declare namespace BeeperDesktop { export import Cursor = Pagination.Cursor; export { type CursorParams as CursorParams, type CursorResponse as CursorResponse }; - export { Accounts as Accounts, type Account as Account, type AccountListResponse as AccountListResponse }; - export { - App as App, - type AppDownloadAssetResponse as AppDownloadAssetResponse, - type AppOpenResponse as AppOpenResponse, - type AppSearchResponse as AppSearchResponse, - type AppDownloadAssetParams as AppDownloadAssetParams, - type AppOpenParams as AppOpenParams, - type AppSearchParams as AppSearchParams, + type DownloadAssetResponse as DownloadAssetResponse, + type OpenResponse as OpenResponse, + type SearchResponse as SearchResponse, + type DownloadAssetParams as DownloadAssetParams, + type OpenParams as OpenParams, + type SearchParams as SearchParams, }; + export { Accounts as Accounts, type Account as Account, type AccountListResponse as AccountListResponse }; + export { Contacts as Contacts, type ContactSearchResponse as ContactSearchResponse, diff --git a/src/resources/app.ts b/src/resources/app.ts deleted file mode 100644 index a7991cf..0000000 --- a/src/resources/app.ts +++ /dev/null @@ -1,178 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { APIResource } from '../core/resource'; -import * as Shared from './shared'; -import * as ChatsAPI from './chats/chats'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -/** - * App operations - */ -export class App extends APIResource { - /** - * Download a Matrix asset using its mxc:// or localmxc:// URL and return the local - * file URL. - * - * @example - * ```ts - * const response = await client.app.downloadAsset({ - * url: 'x', - * }); - * ``` - */ - downloadAsset( - body: AppDownloadAssetParams, - options?: RequestOptions, - ): APIPromise { - return this._client.post('/v0/download-asset', { body, ...options }); - } - - /** - * Open Beeper Desktop and optionally navigate to a specific chat, message, or - * pre-fill draft text and attachment. - * - * @example - * ```ts - * const response = await client.app.open(); - * ``` - */ - open(body: AppOpenParams | null | undefined = {}, options?: RequestOptions): APIPromise { - return this._client.post('/v0/open-app', { body, ...options }); - } - - /** - * Returns matching chats, participant name matches in groups, and the first page - * of messages in one call. Paginate messages via search-messages. Paginate chats - * via search-chats. Uses the same sorting as the chat search in the app. - * - * @example - * ```ts - * const response = await client.app.search({ query: 'x' }); - * ``` - */ - search(query: AppSearchParams, options?: RequestOptions): APIPromise { - return this._client.get('/v0/search', { query, ...options }); - } -} - -export interface AppDownloadAssetResponse { - /** - * Error message if the download failed. - */ - error?: string; - - /** - * Local file URL to the downloaded asset. - */ - srcURL?: string; -} - -/** - * Response indicating successful app opening. - */ -export interface AppOpenResponse { - /** - * Whether the app was successfully opened/focused. - */ - success: boolean; -} - -export interface AppSearchResponse { - results: AppSearchResponse.Results; -} - -export namespace AppSearchResponse { - export interface Results { - /** - * Top chat results. - */ - chats: Array; - - /** - * Top group results by participant matches. - */ - in_groups: Array; - - messages: Results.Messages; - } - - export namespace Results { - export interface Messages { - /** - * Map of chatID -> chat details for chats referenced in items. - */ - chats: { [key: string]: ChatsAPI.Chat }; - - /** - * True if additional results can be fetched using the provided cursors. - */ - hasMore: boolean; - - /** - * Messages matching the query and filters. - */ - items: Array; - - /** - * Cursor for fetching newer results (use with direction='after'). Opaque string; - * do not inspect. - */ - newestCursor: string | null; - - /** - * Cursor for fetching older results (use with direction='before'). Opaque string; - * do not inspect. - */ - oldestCursor: string | null; - } - } -} - -export interface AppDownloadAssetParams { - /** - * Matrix content URL (mxc:// or localmxc://) for the asset to download. - */ - url: string; -} - -export interface AppOpenParams { - /** - * Optional Beeper chat ID (or local chat ID) to focus after opening the app. If - * omitted, only opens/focuses the app. - */ - chatID?: string; - - /** - * Optional draft attachment path to populate in the message input field. - */ - draftAttachmentPath?: string; - - /** - * Optional draft text to populate in the message input field. - */ - draftText?: string; - - /** - * Optional message ID. Jumps to that message in the chat when opening. - */ - messageID?: string; -} - -export interface AppSearchParams { - /** - * User-typed search text. Literal word matching (NOT semantic). - */ - query: string; -} - -export declare namespace App { - export { - type AppDownloadAssetResponse as AppDownloadAssetResponse, - type AppOpenResponse as AppOpenResponse, - type AppSearchResponse as AppSearchResponse, - type AppDownloadAssetParams as AppDownloadAssetParams, - type AppOpenParams as AppOpenParams, - type AppSearchParams as AppSearchParams, - }; -} diff --git a/src/resources/index.ts b/src/resources/index.ts index 4d7984c..63d03ea 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -2,15 +2,6 @@ export * from './shared'; export { Accounts, type Account, type AccountListResponse } from './accounts'; -export { - App, - type AppDownloadAssetResponse, - type AppOpenResponse, - type AppSearchResponse, - type AppDownloadAssetParams, - type AppOpenParams, - type AppSearchParams, -} from './app'; export { Chats, type Chat, @@ -29,3 +20,11 @@ export { type MessageSendParams, } from './messages'; export { Token, type RevokeRequest, type UserInfo } from './token'; +export { + type DownloadAssetResponse, + type OpenResponse, + type SearchResponse, + type DownloadAssetParams, + type OpenParams, + type SearchParams, +} from './top-level'; diff --git a/src/resources/top-level.ts b/src/resources/top-level.ts new file mode 100644 index 0000000..7a9eafe --- /dev/null +++ b/src/resources/top-level.ts @@ -0,0 +1,125 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import * as Shared from './shared'; +import * as ChatsAPI from './chats/chats'; + +export interface DownloadAssetResponse { + /** + * Error message if the download failed. + */ + error?: string; + + /** + * Local file URL to the downloaded asset. + */ + srcURL?: string; +} + +/** + * Response indicating successful app opening. + */ +export interface OpenResponse { + /** + * Whether the app was successfully opened/focused. + */ + success: boolean; +} + +export interface SearchResponse { + results: SearchResponse.Results; +} + +export namespace SearchResponse { + export interface Results { + /** + * Top chat results. + */ + chats: Array; + + /** + * Top group results by participant matches. + */ + in_groups: Array; + + messages: Results.Messages; + } + + export namespace Results { + export interface Messages { + /** + * Map of chatID -> chat details for chats referenced in items. + */ + chats: { [key: string]: ChatsAPI.Chat }; + + /** + * True if additional results can be fetched using the provided cursors. + */ + hasMore: boolean; + + /** + * Messages matching the query and filters. + */ + items: Array; + + /** + * Cursor for fetching newer results (use with direction='after'). Opaque string; + * do not inspect. + */ + newestCursor: string | null; + + /** + * Cursor for fetching older results (use with direction='before'). Opaque string; + * do not inspect. + */ + oldestCursor: string | null; + } + } +} + +export interface DownloadAssetParams { + /** + * Matrix content URL (mxc:// or localmxc://) for the asset to download. + */ + url: string; +} + +export interface OpenParams { + /** + * Optional Beeper chat ID (or local chat ID) to focus after opening the app. If + * omitted, only opens/focuses the app. + */ + chatID?: string; + + /** + * Optional draft attachment path to populate in the message input field. + */ + draftAttachmentPath?: string; + + /** + * Optional draft text to populate in the message input field. + */ + draftText?: string; + + /** + * Optional message ID. Jumps to that message in the chat when opening. + */ + messageID?: string; +} + +export interface SearchParams { + /** + * User-typed search text. Literal word matching (NOT semantic). + */ + query: string; +} + +export declare namespace TopLevel { + export { + type DownloadAssetResponse as DownloadAssetResponse, + type OpenResponse as OpenResponse, + type SearchResponse as SearchResponse, + type DownloadAssetParams as DownloadAssetParams, + type OpenParams as OpenParams, + type SearchParams as SearchParams, + }; +} diff --git a/tests/api-resources/app.test.ts b/tests/api-resources/top-level.test.ts similarity index 86% rename from tests/api-resources/app.test.ts rename to tests/api-resources/top-level.test.ts index 0e0a118..8035f2f 100644 --- a/tests/api-resources/app.test.ts +++ b/tests/api-resources/top-level.test.ts @@ -7,9 +7,9 @@ const client = new BeeperDesktop({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', }); -describe('resource app', () => { +describe('top level methods', () => { test('downloadAsset: only required params', async () => { - const responsePromise = client.app.downloadAsset({ url: 'x' }); + const responsePromise = client.downloadAsset({ url: 'x' }); const rawResponse = await responsePromise.asResponse(); expect(rawResponse).toBeInstanceOf(Response); const response = await responsePromise; @@ -20,11 +20,11 @@ describe('resource app', () => { }); test('downloadAsset: required and optional params', async () => { - const response = await client.app.downloadAsset({ url: 'x' }); + const response = await client.downloadAsset({ url: 'x' }); }); test('open', async () => { - const responsePromise = client.app.open(); + const responsePromise = client.open(); const rawResponse = await responsePromise.asResponse(); expect(rawResponse).toBeInstanceOf(Response); const response = await responsePromise; @@ -37,7 +37,7 @@ describe('resource app', () => { test('open: 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.app.open( + client.open( { chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', draftAttachmentPath: 'draftAttachmentPath', @@ -50,7 +50,7 @@ describe('resource app', () => { }); test('search: only required params', async () => { - const responsePromise = client.app.search({ query: 'x' }); + const responsePromise = client.search({ query: 'x' }); const rawResponse = await responsePromise.asResponse(); expect(rawResponse).toBeInstanceOf(Response); const response = await responsePromise; @@ -61,6 +61,6 @@ describe('resource app', () => { }); test('search: required and optional params', async () => { - const response = await client.app.search({ query: 'x' }); + const response = await client.search({ query: 'x' }); }); }); From fee94ba4b23ec91abc030ddbb872c08af43e2df8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:55:41 +0000 Subject: [PATCH 18/36] chore: configure new SDK language --- .stats.yml | 2 +- packages/mcp-server/README.md | 20 +++++-- packages/mcp-server/build | 2 +- packages/mcp-server/jest.config.ts | 4 +- packages/mcp-server/manifest.json | 2 +- packages/mcp-server/package.json | 6 +- .../src/tools/accounts/get-accounts.ts | 2 +- .../src/tools/chats/archive-chat.ts | 2 +- .../src/tools/chats/create-chats.ts | 60 +++++++++++++++++++ .../mcp-server/src/tools/chats/get-chat.ts | 2 +- .../chats/reminders/clear-chat-reminder.ts | 2 +- .../chats/reminders/set-chat-reminder.ts | 2 +- .../src/tools/chats/search-chats.ts | 2 +- .../src/tools/contacts/search-contacts.ts | 45 ++++++++++++++ packages/mcp-server/src/tools/index.ts | 8 +++ .../src/tools/messages/search-messages.ts | 2 +- .../src/tools/messages/send-message.ts | 2 +- .../mcp-server/src/tools/token/info-token.ts | 34 +++++++++++ .../tools/top-level/download-asset-client.ts | 38 ++++++++++++ .../src/tools/top-level/open-in-app.ts | 2 +- .../mcp-server/src/tools/top-level/search.ts | 2 +- packages/mcp-server/tsconfig.build.json | 4 +- packages/mcp-server/tsconfig.json | 4 +- 23 files changed, 222 insertions(+), 27 deletions(-) create mode 100644 packages/mcp-server/src/tools/chats/create-chats.ts create mode 100644 packages/mcp-server/src/tools/contacts/search-contacts.ts create mode 100644 packages/mcp-server/src/tools/token/info-token.ts create mode 100644 packages/mcp-server/src/tools/top-level/download-asset-client.ts diff --git a/.stats.yml b/.stats.yml index 85ae457..0cc6155 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 -config_hash: e404c86d86b33ff749bded2ba4d2c725 +config_hash: 62c0fc38bf46dc8cddd3298b7ab75dba diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index a22ced9..21b0416 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -17,7 +17,7 @@ You can run the MCP Server directly via `npx`: ```sh export BEEPER_ACCESS_TOKEN="My Access Token" -npx -y @beeper/desktop-mcp@latest +npx -y @beeper/desktop-api-mcp@latest ``` ### Via MCP Client @@ -32,7 +32,7 @@ For clients with a configuration JSON, it might look something like this: "mcpServers": { "beeper_desktop_api_api": { "command": "npx", - "args": ["-y", "@beeper/desktop-mcp", "--client=claude", "--tools=all"], + "args": ["-y", "@beeper/desktop-api-mcp", "--client=claude", "--tools=all"], "env": { "BEEPER_ACCESS_TOKEN": "My Access Token" } @@ -177,10 +177,10 @@ http://localhost:3000?client=cursor&capability=tool-name-length%3D40 ```js // Import the server, generated endpoints, or the init function -import { server, endpoints, init } from "@beeper/desktop-mcp/server"; +import { server, endpoints, init } from "@beeper/desktop-api-mcp/server"; // import a specific tool -import openInApp from "@beeper/desktop-mcp/tools/top-level/open-in-app"; +import downloadAssetClient from "@beeper/desktop-api-mcp/tools/top-level/download-asset-client"; // initialize the server and all endpoints init({ server, endpoints }); @@ -205,7 +205,7 @@ const myCustomEndpoint = { }; // initialize the server with your custom endpoints -init({ server: myServer, endpoints: [openInApp, myCustomEndpoint] }); +init({ server: myServer, endpoints: [downloadAssetClient, myCustomEndpoint] }); ``` ## Available Tools @@ -214,6 +214,7 @@ The following tools are available in this MCP server. ### Resource `$client`: +- `download_asset_client` (`write`): Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL. - `open_in_app` (`write`) tags: [app]: Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment. - `search` (`read`) tags: [app]: Search for chats, participant name matches in groups, and the first page of messages in one call. Use this when the user asks for a specific chat, group, or person. @@ -221,8 +222,13 @@ The following tools are available in this MCP server. - `get_accounts` (`read`) tags: [accounts]: List connected accounts on this device. Use to pick account context. +### Resource `contacts`: + +- `search_contacts` (`read`): Search users across on a specific account using the network's search API. Only use for creating new chats. + ### Resource `chats`: +- `create_chats` (`write`): Create a single or group chat on a specific account using participant IDs and optional title. - `get_chat` (`read`) tags: [chats]: Get chat details: metadata, participants (limited), last activity. - `archive_chat` (`write`) tags: [chats]: Archive or unarchive a chat. - `search_chats` (`read`) tags: [chats]: Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'. @@ -248,3 +254,7 @@ The following tools are available in this MCP server. • "Who are the participants?" (use scope="participants" in search-chats) Returns: matching messages and referenced chats. - `send_message` (`write`) tags: [messages]: 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 `token`: + +- `info_token` (`read`): Returns information about the authenticated user/token diff --git a/packages/mcp-server/build b/packages/mcp-server/build index b94538a..c8808cc 100644 --- a/packages/mcp-server/build +++ b/packages/mcp-server/build @@ -29,7 +29,7 @@ cp tsconfig.dist-src.json dist/src/tsconfig.json chmod +x dist/index.js -DIST_PATH=./dist PKG_IMPORT_PATH=@beeper/desktop-mcp/ node ../../scripts/utils/postprocess-files.cjs +DIST_PATH=./dist PKG_IMPORT_PATH=@beeper/desktop-api-mcp/ node ../../scripts/utils/postprocess-files.cjs # mcp bundle rm -rf dist-bundle beeper_desktop_api_api.mcpb; mkdir dist-bundle diff --git a/packages/mcp-server/jest.config.ts b/packages/mcp-server/jest.config.ts index 5e54047..f660356 100644 --- a/packages/mcp-server/jest.config.ts +++ b/packages/mcp-server/jest.config.ts @@ -7,8 +7,8 @@ const config: JestConfigWithTsJest = { '^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }], }, moduleNameMapper: { - '^@beeper/desktop-mcp$': '/src/index.ts', - '^@beeper/desktop-mcp/(.*)$': '/src/$1', + '^@beeper/desktop-api-mcp$': '/src/index.ts', + '^@beeper/desktop-api-mcp/(.*)$': '/src/$1', }, modulePathIgnorePatterns: ['/dist/'], testPathIgnorePatterns: ['scripts'], diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 09047c8..212d9d1 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,6 +1,6 @@ { "dxt_version": "0.2", - "name": "@beeper/desktop-mcp", + "name": "@beeper/desktop-api-mcp", "version": "0.1.4", "description": "The official MCP Server for the Beeper Desktop API", "author": { diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index b9510db..9234403 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,5 +1,5 @@ { - "name": "@beeper/desktop-mcp", + "name": "@beeper/desktop-api-mcp", "version": "0.1.5", "description": "The official MCP Server for the Beeper Desktop API", "author": "Beeper Desktop ", @@ -68,8 +68,8 @@ "typescript": "5.8.3" }, "imports": { - "@beeper/desktop-mcp": ".", - "@beeper/desktop-mcp/*": "./src/*" + "@beeper/desktop-api-mcp": ".", + "@beeper/desktop-api-mcp/*": "./src/*" }, "exports": { ".": { diff --git a/packages/mcp-server/src/tools/accounts/get-accounts.ts b/packages/mcp-server/src/tools/accounts/get-accounts.ts index c0aa224..c82da5d 100644 --- a/packages/mcp-server/src/tools/accounts/get-accounts.ts +++ b/packages/mcp-server/src/tools/accounts/get-accounts.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/archive-chat.ts b/packages/mcp-server/src/tools/chats/archive-chat.ts index e0ecebd..269ee3a 100644 --- a/packages/mcp-server/src/tools/chats/archive-chat.ts +++ b/packages/mcp-server/src/tools/chats/archive-chat.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/create-chats.ts b/packages/mcp-server/src/tools/chats/create-chats.ts new file mode 100644 index 0000000..cd46e92 --- /dev/null +++ b/packages/mcp-server/src/tools/chats/create-chats.ts @@ -0,0 +1,60 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/create-chat', + operationId: 'create_chat', +}; + +export const tool: Tool = { + name: 'create_chats', + description: + 'Create a single or group chat on a specific account using participant IDs and optional title.', + inputSchema: { + type: 'object', + properties: { + accountID: { + type: 'string', + description: 'Account to create the chat on.', + }, + participantIDs: { + type: 'array', + description: 'User IDs to include in the new chat.', + items: { + type: 'string', + }, + }, + type: { + type: 'string', + description: + "Chat type to create: 'single' requires exactly one participantID; 'group' supports multiple participants and optional title.", + enum: ['single', 'group'], + }, + messageText: { + type: 'string', + description: 'Optional first message content if the platform requires it to create the chat.', + }, + title: { + type: 'string', + description: 'Optional title for group chats; ignored for single chats on most platforms.', + }, + }, + required: ['accountID', 'participantIDs', 'type'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.chats.create(body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/get-chat.ts b/packages/mcp-server/src/tools/chats/get-chat.ts index c31d6f4..19d5ddc 100644 --- a/packages/mcp-server/src/tools/chats/get-chat.ts +++ b/packages/mcp-server/src/tools/chats/get-chat.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts index 6177be5..ba56879 100644 --- a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts +++ b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts index 521d7fd..bfc1ad4 100644 --- a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts +++ b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/search-chats.ts b/packages/mcp-server/src/tools/chats/search-chats.ts index bc746d9..bdb25de 100644 --- a/packages/mcp-server/src/tools/chats/search-chats.ts +++ b/packages/mcp-server/src/tools/chats/search-chats.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/contacts/search-contacts.ts b/packages/mcp-server/src/tools/contacts/search-contacts.ts new file mode 100644 index 0000000..61a9645 --- /dev/null +++ b/packages/mcp-server/src/tools/contacts/search-contacts.ts @@ -0,0 +1,45 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'contacts', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/v0/search-users', + operationId: 'search_users', +}; + +export const tool: Tool = { + name: 'search_contacts', + description: + "Search users across on a specific account using the network's search API. Only use for creating new chats.", + inputSchema: { + type: 'object', + properties: { + accountID: { + type: 'string', + description: 'Beeper account ID this resource belongs to.', + }, + query: { + type: 'string', + description: 'Text to search users by. Network-specific behavior.', + }, + }, + required: ['accountID', 'query'], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.contacts.search(body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index 4f1144e..d35d250 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -4,9 +4,12 @@ import { Metadata, Endpoint, HandlerFunction } from './types'; export { Metadata, Endpoint, HandlerFunction }; +import download_asset_client from './top-level/download-asset-client'; import open_in_app from './top-level/open-in-app'; import search from './top-level/search'; import get_accounts from './accounts/get-accounts'; +import search_contacts from './contacts/search-contacts'; +import create_chats from './chats/create-chats'; import get_chat from './chats/get-chat'; import archive_chat from './chats/archive-chat'; import search_chats from './chats/search-chats'; @@ -14,6 +17,7 @@ import set_chat_reminder from './chats/reminders/set-chat-reminder'; import clear_chat_reminder from './chats/reminders/clear-chat-reminder'; import search_messages from './messages/search-messages'; import send_message from './messages/send-message'; +import info_token from './token/info-token'; export const endpoints: Endpoint[] = []; @@ -21,9 +25,12 @@ function addEndpoint(endpoint: Endpoint) { endpoints.push(endpoint); } +addEndpoint(download_asset_client); addEndpoint(open_in_app); addEndpoint(search); addEndpoint(get_accounts); +addEndpoint(search_contacts); +addEndpoint(create_chats); addEndpoint(get_chat); addEndpoint(archive_chat); addEndpoint(search_chats); @@ -31,6 +38,7 @@ addEndpoint(set_chat_reminder); addEndpoint(clear_chat_reminder); addEndpoint(search_messages); addEndpoint(send_message); +addEndpoint(info_token); export type Filter = { type: 'resource' | 'operation' | 'tag' | 'tool'; diff --git a/packages/mcp-server/src/tools/messages/search-messages.ts b/packages/mcp-server/src/tools/messages/search-messages.ts index f9ca9c7..36c52e6 100644 --- a/packages/mcp-server/src/tools/messages/search-messages.ts +++ b/packages/mcp-server/src/tools/messages/search-messages.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/messages/send-message.ts b/packages/mcp-server/src/tools/messages/send-message.ts index 93a7e8a..56e32fb 100644 --- a/packages/mcp-server/src/tools/messages/send-message.ts +++ b/packages/mcp-server/src/tools/messages/send-message.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/token/info-token.ts b/packages/mcp-server/src/tools/token/info-token.ts new file mode 100644 index 0000000..2ef864a --- /dev/null +++ b/packages/mcp-server/src/tools/token/info-token.ts @@ -0,0 +1,34 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'token', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/oauth/userinfo', + operationId: 'oauth_get_user_info', +}; + +export const tool: Tool = { + name: 'info_token', + description: 'Returns information about the authenticated user/token', + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + return asTextContentResult(await client.token.info()); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/download-asset-client.ts b/packages/mcp-server/src/tools/top-level/download-asset-client.ts new file mode 100644 index 0000000..f240e0d --- /dev/null +++ b/packages/mcp-server/src/tools/top-level/download-asset-client.ts @@ -0,0 +1,38 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: '$client', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v0/download-asset', + operationId: 'download_asset', +}; + +export const tool: Tool = { + name: 'download_asset_client', + description: 'Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL.', + inputSchema: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'Matrix content URL (mxc:// or localmxc://) for the asset to download.', + }, + }, + required: ['url'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.downloadAsset(body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/open-in-app.ts b/packages/mcp-server/src/tools/top-level/open-in-app.ts index a7c6529..7001d2a 100644 --- a/packages/mcp-server/src/tools/top-level/open-in-app.ts +++ b/packages/mcp-server/src/tools/top-level/open-in-app.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/top-level/search.ts b/packages/mcp-server/src/tools/top-level/search.ts index 5ae68e8..92454e9 100644 --- a/packages/mcp-server/src/tools/top-level/search.ts +++ b/packages/mcp-server/src/tools/top-level/search.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/tsconfig.build.json b/packages/mcp-server/tsconfig.build.json index c1fe977..a346070 100644 --- a/packages/mcp-server/tsconfig.build.json +++ b/packages/mcp-server/tsconfig.build.json @@ -5,8 +5,8 @@ "compilerOptions": { "rootDir": "./dist/src", "paths": { - "@beeper/desktop-mcp/*": ["./dist/src/*"], - "@beeper/desktop-mcp": ["./dist/src/index.ts"] + "@beeper/desktop-api-mcp/*": ["./dist/src/*"], + "@beeper/desktop-api-mcp": ["./dist/src/index.ts"] }, "noEmit": false, "declaration": true, diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index c70b6cc..d7639fe 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -8,8 +8,8 @@ "moduleResolution": "node", "esModuleInterop": true, "paths": { - "@beeper/desktop-mcp/*": ["./src/*"], - "@beeper/desktop-mcp": ["./src/index.ts"] + "@beeper/desktop-api-mcp/*": ["./src/*"], + "@beeper/desktop-api-mcp": ["./src/index.ts"] }, "noEmit": true, From 7b4c119731bcd62670484620afb013bf57dbd34b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:56:04 +0000 Subject: [PATCH 19/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 0cc6155..6c4baf0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 -config_hash: 62c0fc38bf46dc8cddd3298b7ab75dba +config_hash: 64d1986aea34e825a4a842c9da1a0d21 From 3501ff900847979fcf7b6e543a99364b74a979a5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:56:47 +0000 Subject: [PATCH 20/36] feat(api): manual updates --- .stats.yml | 4 +- README.md | 78 +- api.md | 48 - packages/mcp-server/README.md | 83 +- packages/mcp-server/build | 2 +- packages/mcp-server/jest.config.ts | 4 +- packages/mcp-server/manifest.json | 2 +- packages/mcp-server/package.json | 6 +- .../src/tools/accounts/get-accounts.ts | 34 - .../src/tools/chats/archive-chat.ts | 43 - .../src/tools/chats/create-chats.ts | 60 - .../mcp-server/src/tools/chats/get-chat.ts | 46 - .../chats/reminders/clear-chat-reminder.ts | 39 - .../chats/reminders/set-chat-reminder.ts | 53 - .../src/tools/chats/search-chats.ts | 104 - .../src/tools/contacts/search-contacts.ts | 45 - packages/mcp-server/src/tools/index.ts | 30 - .../src/tools/messages/search-messages.ts | 124 - .../src/tools/messages/send-message.ts | 47 - .../mcp-server/src/tools/token/info-token.ts | 34 - .../tools/top-level/download-asset-client.ts | 38 - .../src/tools/top-level/open-in-app.ts | 52 - .../mcp-server/src/tools/top-level/search.ts | 41 - packages/mcp-server/tsconfig.build.json | 4 +- packages/mcp-server/tsconfig.json | 4 +- src/client.ts | 138 +- src/internal/qs/LICENSE.md | 13 - src/internal/qs/README.md | 3 - src/internal/qs/formats.ts | 10 - src/internal/qs/index.ts | 13 - src/internal/qs/stringify.ts | 385 --- src/internal/qs/types.ts | 71 - src/internal/qs/utils.ts | 265 -- src/resources/accounts.ts | 24 +- src/resources/chats/chats.ts | 206 +- src/resources/chats/index.ts | 13 +- src/resources/chats/reminders.ts | 79 +- src/resources/contacts.ts | 36 +- src/resources/index.ts | 32 +- src/resources/messages.ts | 140 +- src/resources/shared.ts | 4 - src/resources/token.ts | 14 +- src/resources/top-level.ts | 125 - tests/api-resources/accounts.test.ts | 21 - tests/api-resources/chats/chats.test.ts | 105 - tests/api-resources/chats/reminders.test.ts | 46 - tests/api-resources/contacts.test.ts | 31 - tests/api-resources/messages.test.ts | 67 - tests/api-resources/top-level.test.ts | 66 - tests/qs/empty-keys-cases.ts | 271 -- tests/qs/stringify.test.ts | 2232 ----------------- tests/qs/utils.test.ts | 169 -- tests/stringifyQuery.test.ts | 6 + 53 files changed, 87 insertions(+), 5523 deletions(-) delete mode 100644 packages/mcp-server/src/tools/accounts/get-accounts.ts delete mode 100644 packages/mcp-server/src/tools/chats/archive-chat.ts delete mode 100644 packages/mcp-server/src/tools/chats/create-chats.ts delete mode 100644 packages/mcp-server/src/tools/chats/get-chat.ts delete mode 100644 packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts delete mode 100644 packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts delete mode 100644 packages/mcp-server/src/tools/chats/search-chats.ts delete mode 100644 packages/mcp-server/src/tools/contacts/search-contacts.ts delete mode 100644 packages/mcp-server/src/tools/messages/search-messages.ts delete mode 100644 packages/mcp-server/src/tools/messages/send-message.ts delete mode 100644 packages/mcp-server/src/tools/token/info-token.ts delete mode 100644 packages/mcp-server/src/tools/top-level/download-asset-client.ts delete mode 100644 packages/mcp-server/src/tools/top-level/open-in-app.ts delete mode 100644 packages/mcp-server/src/tools/top-level/search.ts delete mode 100644 src/internal/qs/LICENSE.md delete mode 100644 src/internal/qs/README.md delete mode 100644 src/internal/qs/formats.ts delete mode 100644 src/internal/qs/index.ts delete mode 100644 src/internal/qs/stringify.ts delete mode 100644 src/internal/qs/types.ts delete mode 100644 src/internal/qs/utils.ts delete mode 100644 src/resources/top-level.ts delete mode 100644 tests/api-resources/accounts.test.ts delete mode 100644 tests/api-resources/chats/chats.test.ts delete mode 100644 tests/api-resources/chats/reminders.test.ts delete mode 100644 tests/api-resources/contacts.test.ts delete mode 100644 tests/api-resources/messages.test.ts delete mode 100644 tests/api-resources/top-level.test.ts delete mode 100644 tests/qs/empty-keys-cases.ts delete mode 100644 tests/qs/stringify.test.ts delete mode 100644 tests/qs/utils.test.ts diff --git a/.stats.yml b/.stats.yml index 6c4baf0..0b6da88 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 14 +configured_endpoints: 1 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 -config_hash: 64d1986aea34e825a4a842c9da1a0d21 +config_hash: 1daafc45928b16902d3d0d547fc99d8e diff --git a/README.md b/README.md index 5f99160..6443d55 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,9 @@ const client = new BeeperDesktop({ accessToken: process.env['BEEPER_ACCESS_TOKEN'], // This is the default and can be omitted }); -const page = await client.chats.search({ includeMuted: true, limit: 3, type: 'single' }); -const chat = page.items[0]; +const userInfo = await client.token.info(); -console.log(chat.id); +console.log(userInfo.sub); ``` ### Request & Response types @@ -42,7 +41,7 @@ const client = new BeeperDesktop({ accessToken: process.env['BEEPER_ACCESS_TOKEN'], // This is the default and can be omitted }); -const accounts: BeeperDesktop.AccountListResponse = await client.accounts.list(); +const userInfo: BeeperDesktop.UserInfo = await client.token.info(); ``` Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors. @@ -55,17 +54,15 @@ a subclass of `APIError` will be thrown: ```ts -const response = await client.messages - .send({ chatID: '1229391', text: 'Hello! Just checking in on the project status.' }) - .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; - } - }); +const userInfo = await client.token.info().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: @@ -97,7 +94,7 @@ const client = new BeeperDesktop({ }); // Or, configure per-request: -await client.accounts.list({ +await client.token.info({ maxRetries: 5, }); ``` @@ -114,7 +111,7 @@ const client = new BeeperDesktop({ }); // Override per-request: -await client.accounts.list({ +await client.token.info({ timeout: 5 * 1000, }); ``` @@ -123,45 +120,6 @@ 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({ - accountIDs: ['local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI'], - limit: 10, - query: 'deployment', - })) { - allMessages.push(message); - } - return allMessages; -} -``` - -Alternatively, you can request a single page at a time: - -```ts -let page = await client.messages.search({ - accountIDs: ['local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI'], - limit: 10, - query: 'deployment', -}); -for (const message of page.items) { - 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) @@ -176,13 +134,13 @@ Unlike `.asResponse()` this method consumes the body, returning once it is parse ```ts const client = new BeeperDesktop(); -const response = await client.accounts.list().asResponse(); +const response = await client.token.info().asResponse(); console.log(response.headers.get('X-My-Header')); console.log(response.statusText); // access the underlying Response object -const { data: accounts, response: raw } = await client.accounts.list().withResponse(); +const { data: userInfo, response: raw } = await client.token.info().withResponse(); console.log(raw.headers.get('X-My-Header')); -console.log(accounts); +console.log(userInfo.sub); ``` ### Logging @@ -262,7 +220,7 @@ parameter. This library doesn't validate at runtime that the request matches the send will be sent as-is. ```ts -client.chats.search({ +client.token.info({ // ... // @ts-expect-error baz is not yet public baz: 'undocumented option', diff --git a/api.md b/api.md index 8b6120c..cc7ad32 100644 --- a/api.md +++ b/api.md @@ -1,17 +1,5 @@ # BeeperDesktop -Types: - -- DownloadAssetResponse -- OpenResponse -- SearchResponse - -Methods: - -- client.downloadAsset({ ...params }) -> DownloadAssetResponse -- client.open({ ...params }) -> OpenResponse -- client.search({ ...params }) -> SearchResponse - # Shared Types: @@ -28,59 +16,23 @@ Types: Types: - Account -- AccountListResponse - -Methods: - -- client.accounts.list() -> AccountListResponse # Contacts -Types: - -- ContactSearchResponse - -Methods: - -- client.contacts.search({ ...params }) -> ContactSearchResponse - # Chats Types: - Chat -- ChatCreateResponse - -Methods: - -- client.chats.create({ ...params }) -> ChatCreateResponse -- client.chats.retrieve({ ...params }) -> Chat -- client.chats.archive({ ...params }) -> BaseResponse -- client.chats.search({ ...params }) -> ChatsCursor ## Reminders -Methods: - -- client.chats.reminders.create({ ...params }) -> BaseResponse -- client.chats.reminders.delete({ ...params }) -> BaseResponse - # Messages -Types: - -- MessageSendResponse - -Methods: - -- client.messages.search({ ...params }) -> MessagesCursor -- client.messages.send({ ...params }) -> MessageSendResponse - # Token Types: -- RevokeRequest - UserInfo Methods: diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 21b0416..8c9032d 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -17,7 +17,7 @@ You can run the MCP Server directly via `npx`: ```sh export BEEPER_ACCESS_TOKEN="My Access Token" -npx -y @beeper/desktop-api-mcp@latest +npx -y @beeper/desktop-mcp@latest ``` ### Via MCP Client @@ -32,7 +32,7 @@ For clients with a configuration JSON, it might look something like this: "mcpServers": { "beeper_desktop_api_api": { "command": "npx", - "args": ["-y", "@beeper/desktop-api-mcp", "--client=claude", "--tools=all"], + "args": ["-y", "@beeper/desktop-mcp", "--client=claude", "--tools=all"], "env": { "BEEPER_ACCESS_TOKEN": "My Access Token" } @@ -59,7 +59,6 @@ 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 -- `--tag` includes a set of endpoints with custom tags provided ### Dynamic tools @@ -176,85 +175,9 @@ 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-mcp/server"; - -// import a specific tool -import downloadAssetClient from "@beeper/desktop-api-mcp/tools/top-level/download-asset-client"; - -// 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: [downloadAssetClient, myCustomEndpoint] }); + ``` ## Available Tools The following tools are available in this MCP server. - -### Resource `$client`: - -- `download_asset_client` (`write`): Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL. -- `open_in_app` (`write`) tags: [app]: Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment. -- `search` (`read`) tags: [app]: Search for chats, participant name matches in groups, and the first page of messages in one call. Use this when the user asks for a specific chat, group, or person. - -### Resource `accounts`: - -- `get_accounts` (`read`) tags: [accounts]: List connected accounts on this device. Use to pick account context. - -### Resource `contacts`: - -- `search_contacts` (`read`): Search users across on a specific account using the network's search API. Only use for creating new chats. - -### Resource `chats`: - -- `create_chats` (`write`): Create a single or group chat on a specific account using participant IDs and optional title. -- `get_chat` (`read`) tags: [chats]: Get chat details: metadata, participants (limited), last activity. -- `archive_chat` (`write`) tags: [chats]: Archive or unarchive a chat. -- `search_chats` (`read`) tags: [chats]: Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'. - -### Resource `chats.reminders`: - -- `set_chat_reminder` (`write`) tags: [chats]: Set a reminder for a chat at a specific time. -- `clear_chat_reminder` (`write`) tags: [chats]: Clear a chat reminder. - -### Resource `messages`: - -- `search_messages` (`read`) tags: [messages]: 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". - - 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 scope="participants" in search-chats) - Returns: matching messages and referenced chats. -- `send_message` (`write`) tags: [messages]: 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 `token`: - -- `info_token` (`read`): Returns information about the authenticated user/token diff --git a/packages/mcp-server/build b/packages/mcp-server/build index c8808cc..b94538a 100644 --- a/packages/mcp-server/build +++ b/packages/mcp-server/build @@ -29,7 +29,7 @@ cp tsconfig.dist-src.json dist/src/tsconfig.json chmod +x dist/index.js -DIST_PATH=./dist PKG_IMPORT_PATH=@beeper/desktop-api-mcp/ node ../../scripts/utils/postprocess-files.cjs +DIST_PATH=./dist PKG_IMPORT_PATH=@beeper/desktop-mcp/ node ../../scripts/utils/postprocess-files.cjs # mcp bundle rm -rf dist-bundle beeper_desktop_api_api.mcpb; mkdir dist-bundle diff --git a/packages/mcp-server/jest.config.ts b/packages/mcp-server/jest.config.ts index f660356..5e54047 100644 --- a/packages/mcp-server/jest.config.ts +++ b/packages/mcp-server/jest.config.ts @@ -7,8 +7,8 @@ const config: JestConfigWithTsJest = { '^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }], }, moduleNameMapper: { - '^@beeper/desktop-api-mcp$': '/src/index.ts', - '^@beeper/desktop-api-mcp/(.*)$': '/src/$1', + '^@beeper/desktop-mcp$': '/src/index.ts', + '^@beeper/desktop-mcp/(.*)$': '/src/$1', }, modulePathIgnorePatterns: ['/dist/'], testPathIgnorePatterns: ['scripts'], diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 212d9d1..09047c8 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,6 +1,6 @@ { "dxt_version": "0.2", - "name": "@beeper/desktop-api-mcp", + "name": "@beeper/desktop-mcp", "version": "0.1.4", "description": "The official MCP Server for the Beeper Desktop API", "author": { diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 9234403..b9510db 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,5 +1,5 @@ { - "name": "@beeper/desktop-api-mcp", + "name": "@beeper/desktop-mcp", "version": "0.1.5", "description": "The official MCP Server for the Beeper Desktop API", "author": "Beeper Desktop ", @@ -68,8 +68,8 @@ "typescript": "5.8.3" }, "imports": { - "@beeper/desktop-api-mcp": ".", - "@beeper/desktop-api-mcp/*": "./src/*" + "@beeper/desktop-mcp": ".", + "@beeper/desktop-mcp/*": "./src/*" }, "exports": { ".": { diff --git a/packages/mcp-server/src/tools/accounts/get-accounts.ts b/packages/mcp-server/src/tools/accounts/get-accounts.ts deleted file mode 100644 index c82da5d..0000000 --- a/packages/mcp-server/src/tools/accounts/get-accounts.ts +++ /dev/null @@ -1,34 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'accounts', - operation: 'read', - tags: ['accounts'], - httpMethod: 'get', - httpPath: '/v0/get-accounts', - operationId: 'get_accounts', -}; - -export const tool: Tool = { - name: 'get_accounts', - description: 'List connected accounts on this device. Use to pick account context.', - inputSchema: { - type: 'object', - properties: {}, - required: [], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - return asTextContentResult(await client.accounts.list()); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/archive-chat.ts b/packages/mcp-server/src/tools/chats/archive-chat.ts deleted file mode 100644 index 269ee3a..0000000 --- a/packages/mcp-server/src/tools/chats/archive-chat.ts +++ /dev/null @@ -1,43 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'chats', - operation: 'write', - tags: ['chats'], - httpMethod: 'post', - httpPath: '/v0/archive-chat', - operationId: 'archive_chat', -}; - -export const tool: Tool = { - name: 'archive_chat', - description: 'Archive or unarchive a chat.', - inputSchema: { - type: 'object', - properties: { - chatID: { - type: 'string', - description: - 'The identifier of the chat to archive or unarchive (accepts both chatID and local chat ID)', - }, - archived: { - type: 'boolean', - description: 'True to archive, false to unarchive', - }, - }, - required: ['chatID'], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.chats.archive(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/create-chats.ts b/packages/mcp-server/src/tools/chats/create-chats.ts deleted file mode 100644 index cd46e92..0000000 --- a/packages/mcp-server/src/tools/chats/create-chats.ts +++ /dev/null @@ -1,60 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'chats', - operation: 'write', - tags: [], - httpMethod: 'post', - httpPath: '/v0/create-chat', - operationId: 'create_chat', -}; - -export const tool: Tool = { - name: 'create_chats', - description: - 'Create a single or group chat on a specific account using participant IDs and optional title.', - inputSchema: { - type: 'object', - properties: { - accountID: { - type: 'string', - description: 'Account to create the chat on.', - }, - participantIDs: { - type: 'array', - description: 'User IDs to include in the new chat.', - items: { - type: 'string', - }, - }, - type: { - type: 'string', - description: - "Chat type to create: 'single' requires exactly one participantID; 'group' supports multiple participants and optional title.", - enum: ['single', 'group'], - }, - messageText: { - type: 'string', - description: 'Optional first message content if the platform requires it to create the chat.', - }, - title: { - type: 'string', - description: 'Optional title for group chats; ignored for single chats on most platforms.', - }, - }, - required: ['accountID', 'participantIDs', 'type'], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.chats.create(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/get-chat.ts b/packages/mcp-server/src/tools/chats/get-chat.ts deleted file mode 100644 index 19d5ddc..0000000 --- a/packages/mcp-server/src/tools/chats/get-chat.ts +++ /dev/null @@ -1,46 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'chats', - operation: 'read', - tags: ['chats'], - httpMethod: 'get', - httpPath: '/v0/get-chat', - operationId: 'get_chat', -}; - -export const tool: Tool = { - name: 'get_chat', - description: 'Get chat details: metadata, participants (limited), last activity.', - 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.', - }, - }, - required: ['chatID'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.chats.retrieve(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts deleted file mode 100644 index ba56879..0000000 --- a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts +++ /dev/null @@ -1,39 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'chats.reminders', - operation: 'write', - tags: ['chats'], - httpMethod: 'post', - httpPath: '/v0/clear-chat-reminder', - operationId: 'clear_chat_reminder', -}; - -export const tool: Tool = { - name: 'clear_chat_reminder', - description: 'Clear a chat reminder.', - inputSchema: { - type: 'object', - properties: { - chatID: { - type: 'string', - description: - 'The identifier of the chat to clear reminder from (accepts both chatID and local chat ID)', - }, - }, - required: ['chatID'], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.chats.reminders.delete(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts deleted file mode 100644 index bfc1ad4..0000000 --- a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts +++ /dev/null @@ -1,53 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'chats.reminders', - operation: 'write', - tags: ['chats'], - httpMethod: 'post', - httpPath: '/v0/set-chat-reminder', - operationId: 'set_chat_reminder', -}; - -export const tool: Tool = { - name: 'set_chat_reminder', - description: 'Set a reminder for a chat at a specific time.', - inputSchema: { - type: 'object', - properties: { - chatID: { - type: 'string', - description: 'The identifier of the chat to set reminder for (accepts both chatID and local chat ID)', - }, - 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'], - }, - }, - required: ['chatID', 'reminder'], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.chats.reminders.create(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/search-chats.ts b/packages/mcp-server/src/tools/chats/search-chats.ts deleted file mode 100644 index bdb25de..0000000 --- a/packages/mcp-server/src/tools/chats/search-chats.ts +++ /dev/null @@ -1,104 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'chats', - operation: 'read', - tags: ['chats'], - httpMethod: 'get', - httpPath: '/v0/search-chats', - operationId: 'search_chats', -}; - -export const tool: Tool = { - name: 'search_chats', - description: - "Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'.", - 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', - description: 'Beeper account ID this resource belongs to.', - }, - }, - cursor: { - type: 'string', - description: 'Pagination cursor from previous response. Use with direction to navigate results', - }, - direction: { - type: 'string', - description: - 'Pagination direction: "after" for newer page, "before" for older page. Defaults to "before" when only cursor is provided.', - enum: ['after', 'before'], - }, - 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', - }, - query: { - type: 'string', - description: - 'Literal token search (non-semantic). Use single words users type (e.g., "dinner"). When multiple words provided, ALL must match. Case-insensitive.', - }, - scope: { - type: 'string', - description: - "Search scope: 'titles' matches title + network; 'participants' matches participant names.", - enum: ['titles', 'participants'], - }, - type: { - type: 'string', - description: - 'Specify the type of chats to retrieve: use "single" for direct messages, "group" for group chats, or "any" to get all types', - enum: ['single', 'group', 'any'], - }, - unreadOnly: { - type: 'boolean', - description: 'Set to true to only retrieve chats that have unread messages', - }, - }, - required: [], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - const response = await client.chats.search(body).asResponse(); - return asTextContentResult(await response.json()); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/contacts/search-contacts.ts b/packages/mcp-server/src/tools/contacts/search-contacts.ts deleted file mode 100644 index 61a9645..0000000 --- a/packages/mcp-server/src/tools/contacts/search-contacts.ts +++ /dev/null @@ -1,45 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'contacts', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/search-users', - operationId: 'search_users', -}; - -export const tool: Tool = { - name: 'search_contacts', - description: - "Search users across on a specific account using the network's search API. Only use for creating new chats.", - inputSchema: { - type: 'object', - properties: { - accountID: { - type: 'string', - description: 'Beeper account ID this resource belongs to.', - }, - query: { - type: 'string', - description: 'Text to search users by. Network-specific behavior.', - }, - }, - required: ['accountID', 'query'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.contacts.search(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index d35d250..1e1c40a 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -4,42 +4,12 @@ import { Metadata, Endpoint, HandlerFunction } from './types'; export { Metadata, Endpoint, HandlerFunction }; -import download_asset_client from './top-level/download-asset-client'; -import open_in_app from './top-level/open-in-app'; -import search from './top-level/search'; -import get_accounts from './accounts/get-accounts'; -import search_contacts from './contacts/search-contacts'; -import create_chats from './chats/create-chats'; -import get_chat from './chats/get-chat'; -import archive_chat from './chats/archive-chat'; -import search_chats from './chats/search-chats'; -import set_chat_reminder from './chats/reminders/set-chat-reminder'; -import clear_chat_reminder from './chats/reminders/clear-chat-reminder'; -import search_messages from './messages/search-messages'; -import send_message from './messages/send-message'; -import info_token from './token/info-token'; - export const endpoints: Endpoint[] = []; function addEndpoint(endpoint: Endpoint) { endpoints.push(endpoint); } -addEndpoint(download_asset_client); -addEndpoint(open_in_app); -addEndpoint(search); -addEndpoint(get_accounts); -addEndpoint(search_contacts); -addEndpoint(create_chats); -addEndpoint(get_chat); -addEndpoint(archive_chat); -addEndpoint(search_chats); -addEndpoint(set_chat_reminder); -addEndpoint(clear_chat_reminder); -addEndpoint(search_messages); -addEndpoint(send_message); -addEndpoint(info_token); - export type Filter = { type: 'resource' | 'operation' | 'tag' | 'tool'; op: 'include' | 'exclude'; diff --git a/packages/mcp-server/src/tools/messages/search-messages.ts b/packages/mcp-server/src/tools/messages/search-messages.ts deleted file mode 100644 index 36c52e6..0000000 --- a/packages/mcp-server/src/tools/messages/search-messages.ts +++ /dev/null @@ -1,124 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'messages', - operation: 'read', - tags: ['messages'], - 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- 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 scope="participants" in search-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', - description: 'Beeper account ID this resource belongs to.', - }, - }, - 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'], - }, - cursor: { - type: 'string', - description: "Opaque pagination cursor; do not inspect. Use together with 'direction'.", - }, - 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', - }, - direction: { - type: 'string', - description: - "Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided.", - enum: ['after', 'before'], - }, - 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 20. The current implementation caps each page at 20 items even if a higher limit is requested.', - }, - mediaTypes: { - type: 'array', - description: - "Filter messages by media types. Use ['any'] for any media type, or specify exact types like ['video', 'image']. Omit for no media filtering.", - items: { - type: 'string', - enum: ['any', 'video', 'image', 'link', 'file'], - }, - }, - 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).", - }, - }, - 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-message.ts b/packages/mcp-server/src/tools/messages/send-message.ts deleted file mode 100644 index 56e32fb..0000000 --- a/packages/mcp-server/src/tools/messages/send-message.ts +++ /dev/null @@ -1,47 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'messages', - operation: 'write', - tags: ['messages'], - httpMethod: 'post', - httpPath: '/v0/send-message', - operationId: 'send_message', -}; - -export const tool: Tool = { - name: 'send_message', - description: - 'Send a text message to a specific chat. Supports replying to existing messages. Returns the sent message ID and a deeplink to the chat', - inputSchema: { - type: 'object', - properties: { - chatID: { - type: 'string', - description: 'Unique identifier of the chat (a.k.a. room or thread).', - }, - 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.', - }, - }, - required: ['chatID'], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.messages.send(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/token/info-token.ts b/packages/mcp-server/src/tools/token/info-token.ts deleted file mode 100644 index 2ef864a..0000000 --- a/packages/mcp-server/src/tools/token/info-token.ts +++ /dev/null @@ -1,34 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'token', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/oauth/userinfo', - operationId: 'oauth_get_user_info', -}; - -export const tool: Tool = { - name: 'info_token', - description: 'Returns information about the authenticated user/token', - inputSchema: { - type: 'object', - properties: {}, - required: [], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - return asTextContentResult(await client.token.info()); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/download-asset-client.ts b/packages/mcp-server/src/tools/top-level/download-asset-client.ts deleted file mode 100644 index f240e0d..0000000 --- a/packages/mcp-server/src/tools/top-level/download-asset-client.ts +++ /dev/null @@ -1,38 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: '$client', - operation: 'write', - tags: [], - httpMethod: 'post', - httpPath: '/v0/download-asset', - operationId: 'download_asset', -}; - -export const tool: Tool = { - name: 'download_asset_client', - description: 'Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL.', - inputSchema: { - type: 'object', - properties: { - url: { - type: 'string', - description: 'Matrix content URL (mxc:// or localmxc://) for the asset to download.', - }, - }, - required: ['url'], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.downloadAsset(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/open-in-app.ts b/packages/mcp-server/src/tools/top-level/open-in-app.ts deleted file mode 100644 index 7001d2a..0000000 --- a/packages/mcp-server/src/tools/top-level/open-in-app.ts +++ /dev/null @@ -1,52 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: '$client', - operation: 'write', - tags: ['app'], - httpMethod: 'post', - httpPath: '/v0/open-app', - operationId: 'open_app', -}; - -export const tool: Tool = { - name: 'open_in_app', - description: - 'Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment.', - inputSchema: { - type: 'object', - properties: { - chatID: { - type: 'string', - description: - 'Optional Beeper chat ID (or local chat ID) to focus after opening the app. If omitted, only opens/focuses the app.', - }, - draftAttachmentPath: { - type: 'string', - description: 'Optional draft attachment path to populate in the message input field.', - }, - draftText: { - type: 'string', - description: 'Optional draft text to populate in the message input field.', - }, - messageID: { - type: 'string', - description: 'Optional message ID. Jumps to that message in the chat when opening.', - }, - }, - required: [], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.open(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/search.ts b/packages/mcp-server/src/tools/top-level/search.ts deleted file mode 100644 index 92454e9..0000000 --- a/packages/mcp-server/src/tools/top-level/search.ts +++ /dev/null @@ -1,41 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: '$client', - operation: 'read', - tags: ['app'], - httpMethod: 'get', - httpPath: '/v0/search', - operationId: 'search', -}; - -export const tool: Tool = { - name: 'search', - description: - 'Search for chats, participant name matches in groups, and the first page of messages in one call. Use this when the user asks for a specific chat, group, or person.', - inputSchema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'User-typed search text. Literal word matching (NOT semantic).', - }, - }, - required: ['query'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.search(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/tsconfig.build.json b/packages/mcp-server/tsconfig.build.json index a346070..c1fe977 100644 --- a/packages/mcp-server/tsconfig.build.json +++ b/packages/mcp-server/tsconfig.build.json @@ -5,8 +5,8 @@ "compilerOptions": { "rootDir": "./dist/src", "paths": { - "@beeper/desktop-api-mcp/*": ["./dist/src/*"], - "@beeper/desktop-api-mcp": ["./dist/src/index.ts"] + "@beeper/desktop-mcp/*": ["./dist/src/*"], + "@beeper/desktop-mcp": ["./dist/src/index.ts"] }, "noEmit": false, "declaration": true, diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index d7639fe..c70b6cc 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -8,8 +8,8 @@ "moduleResolution": "node", "esModuleInterop": true, "paths": { - "@beeper/desktop-api-mcp/*": ["./src/*"], - "@beeper/desktop-api-mcp": ["./src/index.ts"] + "@beeper/desktop-mcp/*": ["./src/*"], + "@beeper/desktop-mcp": ["./src/index.ts"] }, "noEmit": true, diff --git a/src/client.ts b/src/client.ts index 135d166..3802ddb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,37 +11,18 @@ import type { APIResponseProps } from './internal/parse'; import { getPlatformHeaders } from './internal/detect-platform'; 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 Pagination from './core/pagination'; import { AbstractPage, type CursorParams, CursorResponse } from './core/pagination'; import * as Uploads from './core/uploads'; import * as API from './resources/index'; -import * as TopLevelAPI from './resources/top-level'; -import { - DownloadAssetParams, - DownloadAssetResponse, - OpenParams, - OpenResponse, - SearchParams, - SearchResponse, -} from './resources/top-level'; import { APIPromise } from './core/api-promise'; -import { Account, AccountListResponse, Accounts } from './resources/accounts'; -import { ContactSearchParams, ContactSearchResponse, Contacts } from './resources/contacts'; -import { MessageSearchParams, MessageSendParams, MessageSendResponse, Messages } from './resources/messages'; -import { RevokeRequest, Token, UserInfo } from './resources/token'; -import { - Chat, - ChatArchiveParams, - ChatCreateParams, - ChatCreateResponse, - ChatRetrieveParams, - ChatSearchParams, - Chats, - ChatsCursor, -} from './resources/chats/chats'; +import { Account, Accounts } from './resources/accounts'; +import { Contacts } from './resources/contacts'; +import { Messages } from './resources/messages'; +import { Token, UserInfo } from './resources/token'; +import { Chat, Chats } from './resources/chats/chats'; import { type Fetch } from './internal/builtin-types'; import { isRunningInBrowser } from './internal/detect-platform'; import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers'; @@ -237,52 +218,6 @@ export class BeeperDesktop { return this.baseURL !== 'http://localhost:23373'; } - /** - * Download a Matrix asset using its mxc:// or localmxc:// URL and return the local - * file URL. - * - * @example - * ```ts - * const response = await client.downloadAsset({ url: 'x' }); - * ``` - */ - downloadAsset( - body: TopLevelAPI.DownloadAssetParams, - options?: RequestOptions, - ): APIPromise { - return this.post('/v0/download-asset', { body, ...options }); - } - - /** - * Open Beeper Desktop and optionally navigate to a specific chat, message, or - * pre-fill draft text and attachment. - * - * @example - * ```ts - * const response = await client.open(); - * ``` - */ - open( - body: TopLevelAPI.OpenParams | null | undefined = {}, - options?: RequestOptions, - ): APIPromise { - return this.post('/v0/open-app', { body, ...options }); - } - - /** - * Returns matching chats, participant name matches in groups, and the first page - * of messages in one call. Paginate messages via search-messages. Paginate chats - * via search-chats. Uses the same sorting as the chat search in the app. - * - * @example - * ```ts - * const response = await client.search({ query: 'x' }); - * ``` - */ - search(query: TopLevelAPI.SearchParams, options?: RequestOptions): APIPromise { - return this.get('/v0/search', { query, ...options }); - } - protected defaultQuery(): Record | undefined { return this._options.defaultQuery; } @@ -295,8 +230,24 @@ export class BeeperDesktop { return buildHeaders([{ Authorization: `Bearer ${this.accessToken}` }]); } + /** + * Basic re-implementation of `qs.stringify` for primitive types. + */ protected stringifyQuery(query: Record): string { - return qs.stringify(query, { arrayFormat: 'repeat' }); + return Object.entries(query) + .filter(([_, value]) => typeof value !== 'undefined') + .map(([key, value]) => { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } + if (value === null) { + return `${encodeURIComponent(key)}=`; + } + throw new Errors.BeeperDesktopError( + `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, + ); + }) + .join('&'); } private getUserAgent(): string { @@ -836,42 +787,15 @@ export declare namespace BeeperDesktop { export import Cursor = Pagination.Cursor; export { type CursorParams as CursorParams, type CursorResponse as CursorResponse }; - export { - type DownloadAssetResponse as DownloadAssetResponse, - type OpenResponse as OpenResponse, - type SearchResponse as SearchResponse, - type DownloadAssetParams as DownloadAssetParams, - type OpenParams as OpenParams, - type SearchParams as SearchParams, - }; - - export { Accounts as Accounts, type Account as Account, type AccountListResponse as AccountListResponse }; - - export { - Contacts as Contacts, - type ContactSearchResponse as ContactSearchResponse, - type ContactSearchParams as ContactSearchParams, - }; - - export { - Chats as Chats, - type Chat as Chat, - type ChatCreateResponse as ChatCreateResponse, - type ChatsCursor as ChatsCursor, - type ChatCreateParams as ChatCreateParams, - type ChatRetrieveParams as ChatRetrieveParams, - type ChatArchiveParams as ChatArchiveParams, - type ChatSearchParams as ChatSearchParams, - }; - - export { - Messages as Messages, - type MessageSendResponse as MessageSendResponse, - type MessageSearchParams as MessageSearchParams, - type MessageSendParams as MessageSendParams, - }; - - export { Token as Token, type RevokeRequest as RevokeRequest, type UserInfo as UserInfo }; + export { Accounts as Accounts, type Account as Account }; + + export { Contacts as Contacts }; + + export { Chats as Chats, type Chat as Chat }; + + export { Messages as Messages }; + + export { Token as Token, type UserInfo as UserInfo }; export type Attachment = API.Attachment; export type BaseResponse = API.BaseResponse; diff --git a/src/internal/qs/LICENSE.md b/src/internal/qs/LICENSE.md deleted file mode 100644 index 3fda157..0000000 --- a/src/internal/qs/LICENSE.md +++ /dev/null @@ -1,13 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/puruvj/neoqs/graphs/contributors) All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/internal/qs/README.md b/src/internal/qs/README.md deleted file mode 100644 index 67ae04e..0000000 --- a/src/internal/qs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# qs - -This is a vendored version of [neoqs](https://github.com/PuruVJ/neoqs) which is a TypeScript rewrite of [qs](https://github.com/ljharb/qs), a query string library. diff --git a/src/internal/qs/formats.ts b/src/internal/qs/formats.ts deleted file mode 100644 index e76a742..0000000 --- a/src/internal/qs/formats.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Format } from './types'; - -export const default_format: Format = 'RFC3986'; -export const default_formatter = (v: PropertyKey) => String(v); -export const formatters: Record string> = { - RFC1738: (v: PropertyKey) => String(v).replace(/%20/g, '+'), - RFC3986: default_formatter, -}; -export const RFC1738 = 'RFC1738'; -export const RFC3986 = 'RFC3986'; diff --git a/src/internal/qs/index.ts b/src/internal/qs/index.ts deleted file mode 100644 index c3a3620..0000000 --- a/src/internal/qs/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { default_format, formatters, RFC1738, RFC3986 } from './formats'; - -const formats = { - formatters, - RFC1738, - RFC3986, - default: default_format, -}; - -export { stringify } from './stringify'; -export { formats }; - -export type { DefaultDecoder, DefaultEncoder, Format, ParseOptions, StringifyOptions } from './types'; diff --git a/src/internal/qs/stringify.ts b/src/internal/qs/stringify.ts deleted file mode 100644 index 7e71387..0000000 --- a/src/internal/qs/stringify.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { encode, is_buffer, maybe_map, has } from './utils'; -import { default_format, default_formatter, formatters } from './formats'; -import type { NonNullableProperties, StringifyOptions } from './types'; -import { isArray } from '../utils/values'; - -const array_prefix_generators = { - brackets(prefix: PropertyKey) { - return String(prefix) + '[]'; - }, - comma: 'comma', - indices(prefix: PropertyKey, key: string) { - return String(prefix) + '[' + key + ']'; - }, - repeat(prefix: PropertyKey) { - return String(prefix); - }, -}; - -const push_to_array = function (arr: any[], value_or_array: any) { - Array.prototype.push.apply(arr, isArray(value_or_array) ? value_or_array : [value_or_array]); -}; - -let toISOString; - -const defaults = { - addQueryPrefix: false, - allowDots: false, - allowEmptyArrays: false, - arrayFormat: 'indices', - charset: 'utf-8', - charsetSentinel: false, - delimiter: '&', - encode: true, - encodeDotInKeys: false, - encoder: encode, - encodeValuesOnly: false, - format: default_format, - formatter: default_formatter, - /** @deprecated */ - indices: false, - serializeDate(date) { - return (toISOString ??= Function.prototype.call.bind(Date.prototype.toISOString))(date); - }, - skipNulls: false, - strictNullHandling: false, -} as NonNullableProperties; - -function is_non_nullish_primitive(v: unknown): v is string | number | boolean | symbol | bigint { - return ( - typeof v === 'string' || - typeof v === 'number' || - typeof v === 'boolean' || - typeof v === 'symbol' || - typeof v === 'bigint' - ); -} - -const sentinel = {}; - -function inner_stringify( - object: any, - prefix: PropertyKey, - generateArrayPrefix: StringifyOptions['arrayFormat'] | ((prefix: string, key: string) => string), - commaRoundTrip: boolean, - allowEmptyArrays: boolean, - strictNullHandling: boolean, - skipNulls: boolean, - encodeDotInKeys: boolean, - encoder: StringifyOptions['encoder'], - filter: StringifyOptions['filter'], - sort: StringifyOptions['sort'], - allowDots: StringifyOptions['allowDots'], - serializeDate: StringifyOptions['serializeDate'], - format: StringifyOptions['format'], - formatter: StringifyOptions['formatter'], - encodeValuesOnly: boolean, - charset: StringifyOptions['charset'], - sideChannel: WeakMap, -) { - let obj = object; - - let tmp_sc = sideChannel; - let step = 0; - let find_flag = false; - while ((tmp_sc = tmp_sc.get(sentinel)) !== void undefined && !find_flag) { - // Where object last appeared in the ref tree - const pos = tmp_sc.get(object); - step += 1; - if (typeof pos !== 'undefined') { - if (pos === step) { - throw new RangeError('Cyclic object value'); - } else { - find_flag = true; // Break while - } - } - if (typeof tmp_sc.get(sentinel) === 'undefined') { - step = 0; - } - } - - if (typeof filter === 'function') { - obj = filter(prefix, obj); - } else if (obj instanceof Date) { - obj = serializeDate?.(obj); - } else if (generateArrayPrefix === 'comma' && isArray(obj)) { - obj = maybe_map(obj, function (value) { - if (value instanceof Date) { - return serializeDate?.(value); - } - return value; - }); - } - - if (obj === null) { - if (strictNullHandling) { - return encoder && !encodeValuesOnly ? - // @ts-expect-error - encoder(prefix, defaults.encoder, charset, 'key', format) - : prefix; - } - - obj = ''; - } - - if (is_non_nullish_primitive(obj) || is_buffer(obj)) { - if (encoder) { - const key_value = - encodeValuesOnly ? prefix - // @ts-expect-error - : encoder(prefix, defaults.encoder, charset, 'key', format); - return [ - formatter?.(key_value) + - '=' + - // @ts-expect-error - formatter?.(encoder(obj, defaults.encoder, charset, 'value', format)), - ]; - } - return [formatter?.(prefix) + '=' + formatter?.(String(obj))]; - } - - const values: string[] = []; - - if (typeof obj === 'undefined') { - return values; - } - - let obj_keys; - if (generateArrayPrefix === 'comma' && isArray(obj)) { - // we need to join elements in - if (encodeValuesOnly && encoder) { - // @ts-expect-error values only - obj = maybe_map(obj, encoder); - } - obj_keys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }]; - } else if (isArray(filter)) { - obj_keys = filter; - } else { - const keys = Object.keys(obj); - obj_keys = sort ? keys.sort(sort) : keys; - } - - const encoded_prefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix); - - const adjusted_prefix = - commaRoundTrip && isArray(obj) && obj.length === 1 ? encoded_prefix + '[]' : encoded_prefix; - - if (allowEmptyArrays && isArray(obj) && obj.length === 0) { - return adjusted_prefix + '[]'; - } - - for (let j = 0; j < obj_keys.length; ++j) { - const key = obj_keys[j]; - const value = - // @ts-ignore - typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key as any]; - - if (skipNulls && value === null) { - continue; - } - - // @ts-ignore - const encoded_key = allowDots && encodeDotInKeys ? (key as any).replace(/\./g, '%2E') : key; - const key_prefix = - isArray(obj) ? - typeof generateArrayPrefix === 'function' ? - generateArrayPrefix(adjusted_prefix, encoded_key) - : adjusted_prefix - : adjusted_prefix + (allowDots ? '.' + encoded_key : '[' + encoded_key + ']'); - - sideChannel.set(object, step); - const valueSideChannel = new WeakMap(); - valueSideChannel.set(sentinel, sideChannel); - push_to_array( - values, - inner_stringify( - value, - key_prefix, - generateArrayPrefix, - commaRoundTrip, - allowEmptyArrays, - strictNullHandling, - skipNulls, - encodeDotInKeys, - // @ts-ignore - generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder, - filter, - sort, - allowDots, - serializeDate, - format, - formatter, - encodeValuesOnly, - charset, - valueSideChannel, - ), - ); - } - - return values; -} - -function normalize_stringify_options( - opts: StringifyOptions = defaults, -): NonNullableProperties> & { indices?: boolean } { - if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') { - throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided'); - } - - if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') { - throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided'); - } - - if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') { - throw new TypeError('Encoder has to be a function.'); - } - - const charset = opts.charset || defaults.charset; - if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { - throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'); - } - - let format = default_format; - if (typeof opts.format !== 'undefined') { - if (!has(formatters, opts.format)) { - throw new TypeError('Unknown format option provided.'); - } - format = opts.format; - } - const formatter = formatters[format]; - - let filter = defaults.filter; - if (typeof opts.filter === 'function' || isArray(opts.filter)) { - filter = opts.filter; - } - - let arrayFormat: StringifyOptions['arrayFormat']; - if (opts.arrayFormat && opts.arrayFormat in array_prefix_generators) { - arrayFormat = opts.arrayFormat; - } else if ('indices' in opts) { - arrayFormat = opts.indices ? 'indices' : 'repeat'; - } else { - arrayFormat = defaults.arrayFormat; - } - - if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') { - throw new TypeError('`commaRoundTrip` must be a boolean, or absent'); - } - - const allowDots = - typeof opts.allowDots === 'undefined' ? - !!opts.encodeDotInKeys === true ? - true - : defaults.allowDots - : !!opts.allowDots; - - return { - addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix, - // @ts-ignore - allowDots: allowDots, - allowEmptyArrays: - typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays, - arrayFormat: arrayFormat, - charset: charset, - charsetSentinel: - typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, - commaRoundTrip: !!opts.commaRoundTrip, - delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter, - encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode, - encodeDotInKeys: - typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys, - encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder, - encodeValuesOnly: - typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly, - filter: filter, - format: format, - formatter: formatter, - serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate, - skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls, - // @ts-ignore - sort: typeof opts.sort === 'function' ? opts.sort : null, - strictNullHandling: - typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling, - }; -} - -export function stringify(object: any, opts: StringifyOptions = {}) { - let obj = object; - const options = normalize_stringify_options(opts); - - let obj_keys: PropertyKey[] | undefined; - let filter; - - if (typeof options.filter === 'function') { - filter = options.filter; - obj = filter('', obj); - } else if (isArray(options.filter)) { - filter = options.filter; - obj_keys = filter; - } - - const keys: string[] = []; - - if (typeof obj !== 'object' || obj === null) { - return ''; - } - - const generateArrayPrefix = array_prefix_generators[options.arrayFormat]; - const commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip; - - if (!obj_keys) { - obj_keys = Object.keys(obj); - } - - if (options.sort) { - obj_keys.sort(options.sort); - } - - const sideChannel = new WeakMap(); - for (let i = 0; i < obj_keys.length; ++i) { - const key = obj_keys[i]!; - - if (options.skipNulls && obj[key] === null) { - continue; - } - push_to_array( - keys, - inner_stringify( - obj[key], - key, - // @ts-expect-error - generateArrayPrefix, - commaRoundTrip, - options.allowEmptyArrays, - options.strictNullHandling, - options.skipNulls, - options.encodeDotInKeys, - options.encode ? options.encoder : null, - options.filter, - options.sort, - options.allowDots, - options.serializeDate, - options.format, - options.formatter, - options.encodeValuesOnly, - options.charset, - sideChannel, - ), - ); - } - - const joined = keys.join(options.delimiter); - let prefix = options.addQueryPrefix === true ? '?' : ''; - - if (options.charsetSentinel) { - if (options.charset === 'iso-8859-1') { - // encodeURIComponent('✓'), the "numeric entity" representation of a checkmark - prefix += 'utf8=%26%2310003%3B&'; - } else { - // encodeURIComponent('✓') - prefix += 'utf8=%E2%9C%93&'; - } - } - - return joined.length > 0 ? prefix + joined : ''; -} diff --git a/src/internal/qs/types.ts b/src/internal/qs/types.ts deleted file mode 100644 index 7c28dbb..0000000 --- a/src/internal/qs/types.ts +++ /dev/null @@ -1,71 +0,0 @@ -export type Format = 'RFC1738' | 'RFC3986'; - -export type DefaultEncoder = (str: any, defaultEncoder?: any, charset?: string) => string; -export type DefaultDecoder = (str: string, decoder?: any, charset?: string) => string; - -export type BooleanOptional = boolean | undefined; - -export type StringifyBaseOptions = { - delimiter?: string; - allowDots?: boolean; - encodeDotInKeys?: boolean; - strictNullHandling?: boolean; - skipNulls?: boolean; - encode?: boolean; - encoder?: ( - str: any, - defaultEncoder: DefaultEncoder, - charset: string, - type: 'key' | 'value', - format?: Format, - ) => string; - filter?: Array | ((prefix: PropertyKey, value: any) => any); - arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma'; - indices?: boolean; - sort?: ((a: PropertyKey, b: PropertyKey) => number) | null; - serializeDate?: (d: Date) => string; - format?: 'RFC1738' | 'RFC3986'; - formatter?: (str: PropertyKey) => string; - encodeValuesOnly?: boolean; - addQueryPrefix?: boolean; - charset?: 'utf-8' | 'iso-8859-1'; - charsetSentinel?: boolean; - allowEmptyArrays?: boolean; - commaRoundTrip?: boolean; -}; - -export type StringifyOptions = StringifyBaseOptions; - -export type ParseBaseOptions = { - comma?: boolean; - delimiter?: string | RegExp; - depth?: number | false; - decoder?: (str: string, defaultDecoder: DefaultDecoder, charset: string, type: 'key' | 'value') => any; - arrayLimit?: number; - parseArrays?: boolean; - plainObjects?: boolean; - allowPrototypes?: boolean; - allowSparse?: boolean; - parameterLimit?: number; - strictDepth?: boolean; - strictNullHandling?: boolean; - ignoreQueryPrefix?: boolean; - charset?: 'utf-8' | 'iso-8859-1'; - charsetSentinel?: boolean; - interpretNumericEntities?: boolean; - allowEmptyArrays?: boolean; - duplicates?: 'combine' | 'first' | 'last'; - allowDots?: boolean; - decodeDotInKeys?: boolean; -}; - -export type ParseOptions = ParseBaseOptions; - -export type ParsedQs = { - [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; -}; - -// Type to remove null or undefined union from each property -export type NonNullableProperties = { - [K in keyof T]-?: Exclude; -}; diff --git a/src/internal/qs/utils.ts b/src/internal/qs/utils.ts deleted file mode 100644 index 4cd5657..0000000 --- a/src/internal/qs/utils.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { RFC1738 } from './formats'; -import type { DefaultEncoder, Format } from './types'; -import { isArray } from '../utils/values'; - -export let has = (obj: object, key: PropertyKey): boolean => ( - (has = (Object as any).hasOwn ?? Function.prototype.call.bind(Object.prototype.hasOwnProperty)), - has(obj, key) -); - -const hex_table = /* @__PURE__ */ (() => { - const array = []; - for (let i = 0; i < 256; ++i) { - array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase()); - } - - return array; -})(); - -function compact_queue>(queue: Array<{ obj: T; prop: string }>) { - while (queue.length > 1) { - const item = queue.pop(); - if (!item) continue; - - const obj = item.obj[item.prop]; - - if (isArray(obj)) { - const compacted: unknown[] = []; - - for (let j = 0; j < obj.length; ++j) { - if (typeof obj[j] !== 'undefined') { - compacted.push(obj[j]); - } - } - - // @ts-ignore - item.obj[item.prop] = compacted; - } - } -} - -function array_to_object(source: any[], options: { plainObjects: boolean }) { - const obj = options && options.plainObjects ? Object.create(null) : {}; - for (let i = 0; i < source.length; ++i) { - if (typeof source[i] !== 'undefined') { - obj[i] = source[i]; - } - } - - return obj; -} - -export function merge( - target: any, - source: any, - options: { plainObjects?: boolean; allowPrototypes?: boolean } = {}, -) { - if (!source) { - return target; - } - - if (typeof source !== 'object') { - if (isArray(target)) { - target.push(source); - } else if (target && typeof target === 'object') { - if ((options && (options.plainObjects || options.allowPrototypes)) || !has(Object.prototype, source)) { - target[source] = true; - } - } else { - return [target, source]; - } - - return target; - } - - if (!target || typeof target !== 'object') { - return [target].concat(source); - } - - let mergeTarget = target; - if (isArray(target) && !isArray(source)) { - // @ts-ignore - mergeTarget = array_to_object(target, options); - } - - if (isArray(target) && isArray(source)) { - source.forEach(function (item, i) { - if (has(target, i)) { - const targetItem = target[i]; - if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { - target[i] = merge(targetItem, item, options); - } else { - target.push(item); - } - } else { - target[i] = item; - } - }); - return target; - } - - return Object.keys(source).reduce(function (acc, key) { - const value = source[key]; - - if (has(acc, key)) { - acc[key] = merge(acc[key], value, options); - } else { - acc[key] = value; - } - return acc; - }, mergeTarget); -} - -export function assign_single_source(target: any, source: any) { - return Object.keys(source).reduce(function (acc, key) { - acc[key] = source[key]; - return acc; - }, target); -} - -export function decode(str: string, _: any, charset: string) { - const strWithoutPlus = str.replace(/\+/g, ' '); - if (charset === 'iso-8859-1') { - // unescape never throws, no try...catch needed: - return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape); - } - // utf-8 - try { - return decodeURIComponent(strWithoutPlus); - } catch (e) { - return strWithoutPlus; - } -} - -const limit = 1024; - -export const encode: ( - str: any, - defaultEncoder: DefaultEncoder, - charset: string, - type: 'key' | 'value', - format: Format, -) => string = (str, _defaultEncoder, charset, _kind, format: Format) => { - // This code was originally written by Brian White for the io.js core querystring library. - // It has been adapted here for stricter adherence to RFC 3986 - if (str.length === 0) { - return str; - } - - let string = str; - if (typeof str === 'symbol') { - string = Symbol.prototype.toString.call(str); - } else if (typeof str !== 'string') { - string = String(str); - } - - if (charset === 'iso-8859-1') { - return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { - return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; - }); - } - - let out = ''; - for (let j = 0; j < string.length; j += limit) { - const segment = string.length >= limit ? string.slice(j, j + limit) : string; - const arr = []; - - for (let i = 0; i < segment.length; ++i) { - let c = segment.charCodeAt(i); - if ( - c === 0x2d || // - - c === 0x2e || // . - c === 0x5f || // _ - c === 0x7e || // ~ - (c >= 0x30 && c <= 0x39) || // 0-9 - (c >= 0x41 && c <= 0x5a) || // a-z - (c >= 0x61 && c <= 0x7a) || // A-Z - (format === RFC1738 && (c === 0x28 || c === 0x29)) // ( ) - ) { - arr[arr.length] = segment.charAt(i); - continue; - } - - if (c < 0x80) { - arr[arr.length] = hex_table[c]; - continue; - } - - if (c < 0x800) { - arr[arr.length] = hex_table[0xc0 | (c >> 6)]! + hex_table[0x80 | (c & 0x3f)]; - continue; - } - - if (c < 0xd800 || c >= 0xe000) { - arr[arr.length] = - hex_table[0xe0 | (c >> 12)]! + hex_table[0x80 | ((c >> 6) & 0x3f)] + hex_table[0x80 | (c & 0x3f)]; - continue; - } - - i += 1; - c = 0x10000 + (((c & 0x3ff) << 10) | (segment.charCodeAt(i) & 0x3ff)); - - arr[arr.length] = - hex_table[0xf0 | (c >> 18)]! + - hex_table[0x80 | ((c >> 12) & 0x3f)] + - hex_table[0x80 | ((c >> 6) & 0x3f)] + - hex_table[0x80 | (c & 0x3f)]; - } - - out += arr.join(''); - } - - return out; -}; - -export function compact(value: any) { - const queue = [{ obj: { o: value }, prop: 'o' }]; - const refs = []; - - for (let i = 0; i < queue.length; ++i) { - const item = queue[i]; - // @ts-ignore - const obj = item.obj[item.prop]; - - const keys = Object.keys(obj); - for (let j = 0; j < keys.length; ++j) { - const key = keys[j]!; - const val = obj[key]; - if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { - queue.push({ obj: obj, prop: key }); - refs.push(val); - } - } - } - - compact_queue(queue); - - return value; -} - -export function is_regexp(obj: any) { - return Object.prototype.toString.call(obj) === '[object RegExp]'; -} - -export function is_buffer(obj: any) { - if (!obj || typeof obj !== 'object') { - return false; - } - - return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); -} - -export function combine(a: any, b: any) { - return [].concat(a, b); -} - -export function maybe_map(val: T[], fn: (v: T) => T) { - if (isArray(val)) { - const mapped = []; - for (let i = 0; i < val.length; i += 1) { - mapped.push(fn(val[i]!)); - } - return mapped; - } - return fn(val); -} diff --git a/src/resources/accounts.ts b/src/resources/accounts.ts index 8dcdaf0..6691161 100644 --- a/src/resources/accounts.ts +++ b/src/resources/accounts.ts @@ -2,25 +2,11 @@ import { APIResource } from '../core/resource'; import * as Shared from './shared'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; /** * Accounts operations */ -export class Accounts extends APIResource { - /** - * List connected Beeper accounts available on this device - * - * @example - * ```ts - * const accounts = await client.accounts.list(); - * ``` - */ - list(options?: RequestOptions): APIPromise { - return this._client.get('/v0/get-accounts', options); - } -} +export class Accounts extends APIResource {} /** * A chat account added to Beeper @@ -44,12 +30,6 @@ export interface Account { user: Shared.User; } -/** - * Connected accounts the user can act through. Includes accountID, network, and - * user identity. - */ -export type AccountListResponse = Array; - export declare namespace Accounts { - export { type Account as Account, type AccountListResponse as AccountListResponse }; + export { type Account as Account }; } diff --git a/src/resources/chats/chats.ts b/src/resources/chats/chats.ts index 0154f56..b4218f0 100644 --- a/src/resources/chats/chats.ts +++ b/src/resources/chats/chats.ts @@ -3,86 +3,15 @@ import { APIResource } from '../../core/resource'; import * as Shared from '../shared'; import * as RemindersAPI from './reminders'; -import { ReminderCreateParams, ReminderDeleteParams, Reminders } from './reminders'; -import { APIPromise } from '../../core/api-promise'; -import { Cursor, type CursorParams, PagePromise } from '../../core/pagination'; -import { RequestOptions } from '../../internal/request-options'; +import { Reminders } from './reminders'; /** * Chats operations */ export class Chats extends APIResource { reminders: RemindersAPI.Reminders = new RemindersAPI.Reminders(this._client); - - /** - * Create a single or group chat on a specific account using participant IDs and - * optional title. - * - * @example - * ```ts - * const chat = await client.chats.create({ - * accountID: - * 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', - * participantIDs: ['string'], - * type: 'single', - * }); - * ``` - */ - create(body: ChatCreateParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/create-chat', { body, ...options }); - } - - /** - * Retrieve chat details including metadata, participants, and latest message - * - * @example - * ```ts - * const chat = await client.chats.retrieve({ - * chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', - * }); - * ``` - */ - 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: '!NCdzlIaMjZUmvmvyHU:beeper.com', - * }); - * ``` - */ - archive(body: ChatArchiveParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/archive-chat', { body, ...options }); - } - - /** - * Search chats by title/network or participants using Beeper Desktop's renderer - * algorithm. - * - * @example - * ```ts - * // Automatically fetches more pages as needed. - * for await (const chat of client.chats.search()) { - * // ... - * } - * ``` - */ - search( - query: ChatSearchParams | null | undefined = {}, - options?: RequestOptions, - ): PagePromise { - return this._client.getAPIList('/v0/search-chats', Cursor, { query, ...options }); - } } -export type ChatsCursor = Cursor; - export interface Chat { /** * Unique identifier of the chat (room/thread ID, same as id) across Beeper. @@ -174,139 +103,10 @@ export namespace Chat { } } -export interface ChatCreateResponse extends Shared.BaseResponse { - /** - * Newly created chat if available. - */ - chatID?: string; -} - -export interface ChatCreateParams { - /** - * Account to create the chat on. - */ - accountID: string; - - /** - * User IDs to include in the new chat. - */ - participantIDs: Array; - - /** - * Chat type to create: 'single' requires exactly one participantID; 'group' - * supports multiple participants and optional title. - */ - type: 'single' | 'group'; - - /** - * Optional first message content if the platform requires it to create the chat. - */ - messageText?: string; - - /** - * Optional title for group chats; ignored for single chats on most platforms. - */ - title?: 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 (accepts both chatID and - * local chat ID) - */ - chatID: string; - - /** - * True to archive, false to unarchive - */ - archived?: boolean; -} - -export interface ChatSearchParams extends CursorParams { - /** - * 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 | null; - - /** - * 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; - - /** - * Literal token search (non-semantic). Use single words users type (e.g., - * "dinner"). When multiple words provided, ALL must match. Case-insensitive. - */ - query?: string; - - /** - * Search scope: 'titles' matches title + network; 'participants' matches - * participant names. - */ - scope?: 'titles' | 'participants'; - - /** - * Specify the type of chats to retrieve: use "single" for direct messages, "group" - * for group chats, or "any" to get all types - */ - type?: 'single' | 'group' | 'any'; - - /** - * Set to true to only retrieve chats that have unread messages - */ - unreadOnly?: boolean | null; -} - Chats.Reminders = Reminders; export declare namespace Chats { - export { - type Chat as Chat, - type ChatCreateResponse as ChatCreateResponse, - type ChatsCursor as ChatsCursor, - type ChatCreateParams as ChatCreateParams, - type ChatRetrieveParams as ChatRetrieveParams, - type ChatArchiveParams as ChatArchiveParams, - type ChatSearchParams as ChatSearchParams, - }; + export { type Chat as Chat }; - export { - Reminders as Reminders, - type ReminderCreateParams as ReminderCreateParams, - type ReminderDeleteParams as ReminderDeleteParams, - }; + export { Reminders as Reminders }; } diff --git a/src/resources/chats/index.ts b/src/resources/chats/index.ts index ab2b155..deb6a20 100644 --- a/src/resources/chats/index.ts +++ b/src/resources/chats/index.ts @@ -1,13 +1,4 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export { - Chats, - type Chat, - type ChatCreateResponse, - type ChatCreateParams, - type ChatRetrieveParams, - type ChatArchiveParams, - type ChatSearchParams, - type ChatsCursor, -} from './chats'; -export { Reminders, type ReminderCreateParams, type ReminderDeleteParams } from './reminders'; +export { Chats, type Chat } from './chats'; +export { Reminders } from './reminders'; diff --git a/src/resources/chats/reminders.ts b/src/resources/chats/reminders.ts index 8be2e77..4ec6691 100644 --- a/src/resources/chats/reminders.ts +++ b/src/resources/chats/reminders.ts @@ -1,85 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../../core/resource'; -import * as Shared from '../shared'; -import { APIPromise } from '../../core/api-promise'; -import { RequestOptions } from '../../internal/request-options'; /** * Reminders operations */ -export class Reminders extends APIResource { - /** - * Set a reminder for a chat at a specific time - * - * @example - * ```ts - * const baseResponse = await client.chats.reminders.create({ - * chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', - * reminder: { remindAtMs: 0 }, - * }); - * ``` - */ - create(body: ReminderCreateParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/set-chat-reminder', { body, ...options }); - } - - /** - * Clear an existing reminder from a chat - * - * @example - * ```ts - * const baseResponse = await client.chats.reminders.delete({ - * chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', - * }); - * ``` - */ - delete(body: ReminderDeleteParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/clear-chat-reminder', { body, ...options }); - } -} - -export interface ReminderCreateParams { - /** - * The identifier of the chat to set reminder for (accepts both chatID and local - * chat ID) - */ - chatID: string; - - /** - * Reminder configuration - */ - reminder: ReminderCreateParams.Reminder; -} - -export namespace ReminderCreateParams { - /** - * 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 ReminderDeleteParams { - /** - * The identifier of the chat to clear reminder from (accepts both chatID and local - * chat ID) - */ - chatID: string; -} - -export declare namespace Reminders { - export { - type ReminderCreateParams as ReminderCreateParams, - type ReminderDeleteParams as ReminderDeleteParams, - }; -} +export class Reminders extends APIResource {} diff --git a/src/resources/contacts.ts b/src/resources/contacts.ts index b7b0f4b..bb9a056 100644 --- a/src/resources/contacts.ts +++ b/src/resources/contacts.ts @@ -1,42 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../core/resource'; -import * as Shared from './shared'; -import { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; /** * Contacts operations */ -export class Contacts extends APIResource { - /** - * Search users across on a specific account using the network's search API. Only - * use for creating new chats. - */ - search(query: ContactSearchParams, options?: RequestOptions): APIPromise { - return this._client.get('/v0/search-users', { query, ...options }); - } -} - -export interface ContactSearchResponse { - items: Array; -} - -export interface ContactSearchParams { - /** - * Beeper account ID this resource belongs to. - */ - accountID: string; - - /** - * Text to search users by. Network-specific behavior. - */ - query: string; -} - -export declare namespace Contacts { - export { - type ContactSearchResponse as ContactSearchResponse, - type ContactSearchParams as ContactSearchParams, - }; -} +export class Contacts extends APIResource {} diff --git a/src/resources/index.ts b/src/resources/index.ts index 63d03ea..4979af0 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -1,30 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. export * from './shared'; -export { Accounts, type Account, type AccountListResponse } from './accounts'; -export { - Chats, - type Chat, - type ChatCreateResponse, - type ChatCreateParams, - type ChatRetrieveParams, - type ChatArchiveParams, - type ChatSearchParams, - type ChatsCursor, -} from './chats/chats'; -export { Contacts, type ContactSearchResponse, type ContactSearchParams } from './contacts'; -export { - Messages, - type MessageSendResponse, - type MessageSearchParams, - type MessageSendParams, -} from './messages'; -export { Token, type RevokeRequest, type UserInfo } from './token'; -export { - type DownloadAssetResponse, - type OpenResponse, - type SearchResponse, - type DownloadAssetParams, - type OpenParams, - type SearchParams, -} from './top-level'; +export { Accounts, type Account } from './accounts'; +export { Chats, type Chat } from './chats/chats'; +export { Contacts } from './contacts'; +export { Messages } from './messages'; +export { Token, type UserInfo } from './token'; diff --git a/src/resources/messages.ts b/src/resources/messages.ts index 32dff27..9043fd3 100644 --- a/src/resources/messages.ts +++ b/src/resources/messages.ts @@ -1,146 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../core/resource'; -import * as Shared from './shared'; -import { MessagesCursor } from './shared'; -import { APIPromise } from '../core/api-promise'; -import { Cursor, type CursorParams, PagePromise } from '../core/pagination'; -import { RequestOptions } from '../internal/request-options'; /** * Messages operations */ -export class Messages extends APIResource { - /** - * Search messages across chats using Beeper's message index - * - * @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', Cursor, { query, ...options }); - } - - /** - * Send a text message to a specific chat. Supports replying to existing messages. - * Returns the sent message ID. - * - * @example - * ```ts - * const response = await client.messages.send({ - * chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', - * }); - * ``` - */ - send(body: MessageSendParams, options?: RequestOptions): APIPromise { - return this._client.post('/v0/send-message', { body, ...options }); - } -} - -export interface MessageSendResponse extends Shared.BaseResponse { - /** - * Unique identifier of the chat (a.k.a. room or thread). - */ - chatID: string; - - /** - * Pending message ID - */ - pendingMessageID: string; -} - -export interface MessageSearchParams extends CursorParams { - /** - * 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 | null; - - /** - * 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 | null; - - /** - * Filter messages by media types. Use ['any'] for any media type, or specify exact - * types like ['video', 'image']. Omit for no media filtering. - */ - mediaTypes?: Array<'any' | 'video' | 'image' | 'link' | 'file'>; - - /** - * 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 { - /** - * Unique identifier of the chat (a.k.a. room or thread). - */ - 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 MessageSendResponse as MessageSendResponse, - type MessageSearchParams as MessageSearchParams, - type MessageSendParams as MessageSendParams, - }; -} - -export { type MessagesCursor }; +export class Messages extends APIResource {} diff --git a/src/resources/shared.ts b/src/resources/shared.ts index 2ef2269..8d65847 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -1,7 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Cursor } from '../core/pagination'; - export interface Attachment { /** * Attachment type. @@ -241,5 +239,3 @@ export interface User { */ username?: string; } - -export type MessagesCursor = Cursor; diff --git a/src/resources/token.ts b/src/resources/token.ts index 4a6608d..d165da7 100644 --- a/src/resources/token.ts +++ b/src/resources/token.ts @@ -16,18 +16,6 @@ export class Token extends APIResource { } } -export interface RevokeRequest { - /** - * The token to revoke - */ - token: string; - - /** - * Hint about the type of token being revoked - */ - token_type_hint?: 'access_token'; -} - export interface UserInfo { /** * Issued at timestamp (Unix epoch seconds) @@ -66,5 +54,5 @@ export interface UserInfo { } export declare namespace Token { - export { type RevokeRequest as RevokeRequest, type UserInfo as UserInfo }; + export { type UserInfo as UserInfo }; } diff --git a/src/resources/top-level.ts b/src/resources/top-level.ts deleted file mode 100644 index 7a9eafe..0000000 --- a/src/resources/top-level.ts +++ /dev/null @@ -1,125 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import * as Shared from './shared'; -import * as ChatsAPI from './chats/chats'; - -export interface DownloadAssetResponse { - /** - * Error message if the download failed. - */ - error?: string; - - /** - * Local file URL to the downloaded asset. - */ - srcURL?: string; -} - -/** - * Response indicating successful app opening. - */ -export interface OpenResponse { - /** - * Whether the app was successfully opened/focused. - */ - success: boolean; -} - -export interface SearchResponse { - results: SearchResponse.Results; -} - -export namespace SearchResponse { - export interface Results { - /** - * Top chat results. - */ - chats: Array; - - /** - * Top group results by participant matches. - */ - in_groups: Array; - - messages: Results.Messages; - } - - export namespace Results { - export interface Messages { - /** - * Map of chatID -> chat details for chats referenced in items. - */ - chats: { [key: string]: ChatsAPI.Chat }; - - /** - * True if additional results can be fetched using the provided cursors. - */ - hasMore: boolean; - - /** - * Messages matching the query and filters. - */ - items: Array; - - /** - * Cursor for fetching newer results (use with direction='after'). Opaque string; - * do not inspect. - */ - newestCursor: string | null; - - /** - * Cursor for fetching older results (use with direction='before'). Opaque string; - * do not inspect. - */ - oldestCursor: string | null; - } - } -} - -export interface DownloadAssetParams { - /** - * Matrix content URL (mxc:// or localmxc://) for the asset to download. - */ - url: string; -} - -export interface OpenParams { - /** - * Optional Beeper chat ID (or local chat ID) to focus after opening the app. If - * omitted, only opens/focuses the app. - */ - chatID?: string; - - /** - * Optional draft attachment path to populate in the message input field. - */ - draftAttachmentPath?: string; - - /** - * Optional draft text to populate in the message input field. - */ - draftText?: string; - - /** - * Optional message ID. Jumps to that message in the chat when opening. - */ - messageID?: string; -} - -export interface SearchParams { - /** - * User-typed search text. Literal word matching (NOT semantic). - */ - query: string; -} - -export declare namespace TopLevel { - export { - type DownloadAssetResponse as DownloadAssetResponse, - type OpenResponse as OpenResponse, - type SearchResponse as SearchResponse, - type DownloadAssetParams as DownloadAssetParams, - type OpenParams as OpenParams, - type SearchParams as SearchParams, - }; -} diff --git a/tests/api-resources/accounts.test.ts b/tests/api-resources/accounts.test.ts deleted file mode 100644 index e8917eb..0000000 --- a/tests/api-resources/accounts.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktop from '@beeper/desktop-api'; - -const client = new BeeperDesktop({ - accessToken: 'My Access Token', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource accounts', () => { - test('list', async () => { - const responsePromise = client.accounts.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); - }); -}); diff --git a/tests/api-resources/chats/chats.test.ts b/tests/api-resources/chats/chats.test.ts deleted file mode 100644 index 162c241..0000000 --- a/tests/api-resources/chats/chats.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktop from '@beeper/desktop-api'; - -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('create: only required params', async () => { - const responsePromise = client.chats.create({ - accountID: 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', - participantIDs: ['string'], - type: 'single', - }); - 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('create: required and optional params', async () => { - const response = await client.chats.create({ - accountID: 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', - participantIDs: ['string'], - type: 'single', - messageText: 'messageText', - title: 'title', - }); - }); - - test('retrieve: only required params', async () => { - const responsePromise = client.chats.retrieve({ chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com' }); - 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: '!NCdzlIaMjZUmvmvyHU:beeper.com', - maxParticipantCount: 50, - }); - }); - - test('archive: only required params', async () => { - const responsePromise = client.chats.archive({ chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com' }); - 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: '!NCdzlIaMjZUmvmvyHU:beeper.com', archived: true }); - }); - - test('search', async () => { - const responsePromise = client.chats.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.chats.search( - { - accountIDs: [ - 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', - 'local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI', - ], - cursor: 'eyJvZmZzZXQiOjE3MTk5OTk5OTl9', - direction: 'after', - inbox: 'primary', - includeMuted: true, - lastActivityAfter: '2019-12-27T18:11:19.117Z', - lastActivityBefore: '2019-12-27T18:11:19.117Z', - limit: 1, - query: 'x', - scope: 'titles', - type: 'single', - unreadOnly: true, - }, - { path: '/_stainless_unknown_path' }, - ), - ).rejects.toThrow(BeeperDesktop.NotFoundError); - }); -}); diff --git a/tests/api-resources/chats/reminders.test.ts b/tests/api-resources/chats/reminders.test.ts deleted file mode 100644 index 8bf6fbd..0000000 --- a/tests/api-resources/chats/reminders.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktop from '@beeper/desktop-api'; - -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('create: only required params', async () => { - const responsePromise = client.chats.reminders.create({ - chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', - 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('create: required and optional params', async () => { - const response = await client.chats.reminders.create({ - chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', - reminder: { remindAtMs: 0, dismissOnIncomingMessage: true }, - }); - }); - - test('delete: only required params', async () => { - const responsePromise = client.chats.reminders.delete({ chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com' }); - 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('delete: required and optional params', async () => { - const response = await client.chats.reminders.delete({ chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com' }); - }); -}); diff --git a/tests/api-resources/contacts.test.ts b/tests/api-resources/contacts.test.ts deleted file mode 100644 index b21cda7..0000000 --- a/tests/api-resources/contacts.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktop from '@beeper/desktop-api'; - -const client = new BeeperDesktop({ - accessToken: 'My Access Token', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource contacts', () => { - test('search: only required params', async () => { - const responsePromise = client.contacts.search({ - accountID: 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', - query: 'x', - }); - 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: required and optional params', async () => { - const response = await client.contacts.search({ - accountID: 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', - query: 'x', - }); - }); -}); diff --git a/tests/api-resources/messages.test.ts b/tests/api-resources/messages.test.ts deleted file mode 100644 index 21fafff..0000000 --- a/tests/api-resources/messages.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktop from '@beeper/desktop-api'; - -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('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: ['!NCdzlIaMjZUmvmvyHU:beeper.com', '1231073'], - chatType: 'group', - cursor: '1725489123456|c29tZUltc2dQYWdl', - dateAfter: '2025-08-01T00:00:00Z', - dateBefore: '2025-08-31T23:59:59Z', - direction: 'before', - excludeLowPriority: true, - includeMuted: true, - limit: 20, - mediaTypes: ['any'], - query: 'dinner', - sender: 'me', - }, - { path: '/_stainless_unknown_path' }, - ), - ).rejects.toThrow(BeeperDesktop.NotFoundError); - }); - - test('send: only required params', async () => { - const responsePromise = client.messages.send({ chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com' }); - 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: '!NCdzlIaMjZUmvmvyHU:beeper.com', - replyToMessageID: 'replyToMessageID', - text: 'text', - }); - }); -}); diff --git a/tests/api-resources/top-level.test.ts b/tests/api-resources/top-level.test.ts deleted file mode 100644 index 8035f2f..0000000 --- a/tests/api-resources/top-level.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktop from '@beeper/desktop-api'; - -const client = new BeeperDesktop({ - accessToken: 'My Access Token', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('top level methods', () => { - test('downloadAsset: only required params', async () => { - const responsePromise = client.downloadAsset({ url: 'x' }); - 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('downloadAsset: required and optional params', async () => { - const response = await client.downloadAsset({ url: 'x' }); - }); - - test('open', async () => { - const responsePromise = client.open(); - 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('open: 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.open( - { - chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', - draftAttachmentPath: 'draftAttachmentPath', - draftText: 'draftText', - messageID: 'messageID', - }, - { path: '/_stainless_unknown_path' }, - ), - ).rejects.toThrow(BeeperDesktop.NotFoundError); - }); - - test('search: only required params', async () => { - const responsePromise = client.search({ query: 'x' }); - 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: required and optional params', async () => { - const response = await client.search({ query: 'x' }); - }); -}); diff --git a/tests/qs/empty-keys-cases.ts b/tests/qs/empty-keys-cases.ts deleted file mode 100644 index ea2c1b0..0000000 --- a/tests/qs/empty-keys-cases.ts +++ /dev/null @@ -1,271 +0,0 @@ -export const empty_test_cases = [ - { - input: '&', - with_empty_keys: {}, - stringify_output: { - brackets: '', - indices: '', - repeat: '', - }, - no_empty_keys: {}, - }, - { - input: '&&', - with_empty_keys: {}, - stringify_output: { - brackets: '', - indices: '', - repeat: '', - }, - no_empty_keys: {}, - }, - { - input: '&=', - with_empty_keys: { '': '' }, - stringify_output: { - brackets: '=', - indices: '=', - repeat: '=', - }, - no_empty_keys: {}, - }, - { - input: '&=&', - with_empty_keys: { '': '' }, - stringify_output: { - brackets: '=', - indices: '=', - repeat: '=', - }, - no_empty_keys: {}, - }, - { - input: '&=&=', - with_empty_keys: { '': ['', ''] }, - stringify_output: { - brackets: '[]=&[]=', - indices: '[0]=&[1]=', - repeat: '=&=', - }, - no_empty_keys: {}, - }, - { - input: '&=&=&', - with_empty_keys: { '': ['', ''] }, - stringify_output: { - brackets: '[]=&[]=', - indices: '[0]=&[1]=', - repeat: '=&=', - }, - no_empty_keys: {}, - }, - { - input: '=', - with_empty_keys: { '': '' }, - no_empty_keys: {}, - stringify_output: { - brackets: '=', - indices: '=', - repeat: '=', - }, - }, - { - input: '=&', - with_empty_keys: { '': '' }, - stringify_output: { - brackets: '=', - indices: '=', - repeat: '=', - }, - no_empty_keys: {}, - }, - { - input: '=&&&', - with_empty_keys: { '': '' }, - stringify_output: { - brackets: '=', - indices: '=', - repeat: '=', - }, - no_empty_keys: {}, - }, - { - input: '=&=&=&', - with_empty_keys: { '': ['', '', ''] }, - stringify_output: { - brackets: '[]=&[]=&[]=', - indices: '[0]=&[1]=&[2]=', - repeat: '=&=&=', - }, - no_empty_keys: {}, - }, - { - input: '=&a[]=b&a[1]=c', - with_empty_keys: { '': '', a: ['b', 'c'] }, - stringify_output: { - brackets: '=&a[]=b&a[]=c', - indices: '=&a[0]=b&a[1]=c', - repeat: '=&a=b&a=c', - }, - no_empty_keys: { a: ['b', 'c'] }, - }, - { - input: '=a', - with_empty_keys: { '': 'a' }, - no_empty_keys: {}, - stringify_output: { - brackets: '=a', - indices: '=a', - repeat: '=a', - }, - }, - { - input: 'a==a', - with_empty_keys: { a: '=a' }, - no_empty_keys: { a: '=a' }, - stringify_output: { - brackets: 'a==a', - indices: 'a==a', - repeat: 'a==a', - }, - }, - { - input: '=&a[]=b', - with_empty_keys: { '': '', a: ['b'] }, - stringify_output: { - brackets: '=&a[]=b', - indices: '=&a[0]=b', - repeat: '=&a=b', - }, - no_empty_keys: { a: ['b'] }, - }, - { - input: '=&a[]=b&a[]=c&a[2]=d', - with_empty_keys: { '': '', a: ['b', 'c', 'd'] }, - stringify_output: { - brackets: '=&a[]=b&a[]=c&a[]=d', - indices: '=&a[0]=b&a[1]=c&a[2]=d', - repeat: '=&a=b&a=c&a=d', - }, - no_empty_keys: { a: ['b', 'c', 'd'] }, - }, - { - input: '=a&=b', - with_empty_keys: { '': ['a', 'b'] }, - stringify_output: { - brackets: '[]=a&[]=b', - indices: '[0]=a&[1]=b', - repeat: '=a&=b', - }, - no_empty_keys: {}, - }, - { - input: '=a&foo=b', - with_empty_keys: { '': 'a', foo: 'b' }, - no_empty_keys: { foo: 'b' }, - stringify_output: { - brackets: '=a&foo=b', - indices: '=a&foo=b', - repeat: '=a&foo=b', - }, - }, - { - input: 'a[]=b&a=c&=', - with_empty_keys: { '': '', a: ['b', 'c'] }, - stringify_output: { - brackets: '=&a[]=b&a[]=c', - indices: '=&a[0]=b&a[1]=c', - repeat: '=&a=b&a=c', - }, - no_empty_keys: { a: ['b', 'c'] }, - }, - { - input: 'a[]=b&a=c&=', - with_empty_keys: { '': '', a: ['b', 'c'] }, - stringify_output: { - brackets: '=&a[]=b&a[]=c', - indices: '=&a[0]=b&a[1]=c', - repeat: '=&a=b&a=c', - }, - no_empty_keys: { a: ['b', 'c'] }, - }, - { - input: 'a[0]=b&a=c&=', - with_empty_keys: { '': '', a: ['b', 'c'] }, - stringify_output: { - brackets: '=&a[]=b&a[]=c', - indices: '=&a[0]=b&a[1]=c', - repeat: '=&a=b&a=c', - }, - no_empty_keys: { a: ['b', 'c'] }, - }, - { - input: 'a=b&a[]=c&=', - with_empty_keys: { '': '', a: ['b', 'c'] }, - stringify_output: { - brackets: '=&a[]=b&a[]=c', - indices: '=&a[0]=b&a[1]=c', - repeat: '=&a=b&a=c', - }, - no_empty_keys: { a: ['b', 'c'] }, - }, - { - input: 'a=b&a[0]=c&=', - with_empty_keys: { '': '', a: ['b', 'c'] }, - stringify_output: { - brackets: '=&a[]=b&a[]=c', - indices: '=&a[0]=b&a[1]=c', - repeat: '=&a=b&a=c', - }, - no_empty_keys: { a: ['b', 'c'] }, - }, - { - input: '[]=a&[]=b& []=1', - with_empty_keys: { '': ['a', 'b'], ' ': ['1'] }, - stringify_output: { - brackets: '[]=a&[]=b& []=1', - indices: '[0]=a&[1]=b& [0]=1', - repeat: '=a&=b& =1', - }, - no_empty_keys: { 0: 'a', 1: 'b', ' ': ['1'] }, - }, - { - input: '[0]=a&[1]=b&a[0]=1&a[1]=2', - with_empty_keys: { '': ['a', 'b'], a: ['1', '2'] }, - no_empty_keys: { 0: 'a', 1: 'b', a: ['1', '2'] }, - stringify_output: { - brackets: '[]=a&[]=b&a[]=1&a[]=2', - indices: '[0]=a&[1]=b&a[0]=1&a[1]=2', - repeat: '=a&=b&a=1&a=2', - }, - }, - { - input: '[deep]=a&[deep]=2', - with_empty_keys: { '': { deep: ['a', '2'] } }, - stringify_output: { - brackets: '[deep][]=a&[deep][]=2', - indices: '[deep][0]=a&[deep][1]=2', - repeat: '[deep]=a&[deep]=2', - }, - no_empty_keys: { deep: ['a', '2'] }, - }, - { - input: '%5B0%5D=a&%5B1%5D=b', - with_empty_keys: { '': ['a', 'b'] }, - stringify_output: { - brackets: '[]=a&[]=b', - indices: '[0]=a&[1]=b', - repeat: '=a&=b', - }, - no_empty_keys: { 0: 'a', 1: 'b' }, - }, -] satisfies { - input: string; - with_empty_keys: Record; - stringify_output: { - brackets: string; - indices: string; - repeat: string; - }; - no_empty_keys: Record; -}[]; diff --git a/tests/qs/stringify.test.ts b/tests/qs/stringify.test.ts deleted file mode 100644 index d9e9393..0000000 --- a/tests/qs/stringify.test.ts +++ /dev/null @@ -1,2232 +0,0 @@ -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 { empty_test_cases } from './empty-keys-cases'; -import assert from 'assert'; - -describe('stringify()', function () { - test('stringifies a querystring object', function () { - expect(stringify({ a: 'b' })).toBe('a=b'); - expect(stringify({ a: 1 })).toBe('a=1'); - expect(stringify({ a: 1, b: 2 })).toBe('a=1&b=2'); - expect(stringify({ a: 'A_Z' })).toBe('a=A_Z'); - expect(stringify({ a: '€' })).toBe('a=%E2%82%AC'); - expect(stringify({ a: '' })).toBe('a=%EE%80%80'); - expect(stringify({ a: 'א' })).toBe('a=%D7%90'); - expect(stringify({ a: '𐐷' })).toBe('a=%F0%90%90%B7'); - }); - - test('stringifies falsy values', function () { - expect(stringify(undefined)).toBe(''); - expect(stringify(null)).toBe(''); - expect(stringify(null, { strictNullHandling: true })).toBe(''); - expect(stringify(false)).toBe(''); - expect(stringify(0)).toBe(''); - }); - - test('stringifies symbols', function () { - expect(stringify(Symbol.iterator)).toBe(''); - expect(stringify([Symbol.iterator])).toBe('0=Symbol%28Symbol.iterator%29'); - expect(stringify({ a: Symbol.iterator })).toBe('a=Symbol%28Symbol.iterator%29'); - expect(stringify({ a: [Symbol.iterator] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( - 'a[]=Symbol%28Symbol.iterator%29', - ); - }); - - test('stringifies bigints', function () { - var three = BigInt(3); - // @ts-expect-error - var encodeWithN = function (value, defaultEncoder, charset) { - var result = defaultEncoder(value, defaultEncoder, charset); - return typeof value === 'bigint' ? result + 'n' : result; - }; - - expect(stringify(three)).toBe(''); - expect(stringify([three])).toBe('0=3'); - expect(stringify([three], { encoder: encodeWithN })).toBe('0=3n'); - expect(stringify({ a: three })).toBe('a=3'); - expect(stringify({ a: three }, { encoder: encodeWithN })).toBe('a=3n'); - expect(stringify({ a: [three] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a[]=3'); - expect( - stringify({ a: [three] }, { encodeValuesOnly: true, encoder: encodeWithN, arrayFormat: 'brackets' }), - ).toBe('a[]=3n'); - }); - - test('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function () { - expect( - stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: false, encodeDotInKeys: false }), - ).toBe('name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe'); - expect( - stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: false }), - ).toBe('name.obj.first=John&name.obj.last=Doe'); - expect( - stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: false, encodeDotInKeys: true }), - ).toBe('name%252Eobj%5Bfirst%5D=John&name%252Eobj%5Blast%5D=Doe'); - expect( - stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true }), - ).toBe('name%252Eobj.first=John&name%252Eobj.last=Doe'); - - // st.equal( - // stringify( - // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - // { allowDots: false, encodeDotInKeys: false }, - // ), - // 'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe', - // 'with allowDots false and encodeDotInKeys false', - // ); - // st.equal( - // stringify( - // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - // { allowDots: true, encodeDotInKeys: false }, - // ), - // 'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe', - // 'with allowDots false and encodeDotInKeys false', - // ); - // st.equal( - // stringify( - // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - // { allowDots: false, encodeDotInKeys: true }, - // ), - // 'name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe', - // 'with allowDots false and encodeDotInKeys true', - // ); - // st.equal( - // stringify( - // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - // { allowDots: true, encodeDotInKeys: true }, - // ), - // 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe', - // 'with allowDots true and encodeDotInKeys true', - // ); - expect( - stringify( - { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - { allowDots: false, encodeDotInKeys: false }, - ), - ).toBe('name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe'); - expect( - stringify( - { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - { allowDots: true, encodeDotInKeys: false }, - ), - ).toBe('name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe'); - expect( - stringify( - { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - { allowDots: false, encodeDotInKeys: true }, - ), - ).toBe('name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe'); - expect( - stringify( - { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - { allowDots: true, encodeDotInKeys: true }, - ), - ).toBe('name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe'); - }); - - test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function () { - // st.equal( - // stringify( - // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - // { encodeDotInKeys: true }, - // ), - // 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe', - // 'with allowDots undefined and encodeDotInKeys true', - // ); - expect( - stringify( - { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - { encodeDotInKeys: true }, - ), - ).toBe('name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe'); - }); - - test('should encode dot in key of object when encodeDotInKeys and allowDots is provided, and nothing else when encodeValuesOnly is provided', function () { - // st.equal( - // stringify( - // { 'name.obj': { first: 'John', last: 'Doe' } }, - // { - // encodeDotInKeys: true, - // allowDots: true, - // encodeValuesOnly: true, - // }, - // ), - // 'name%2Eobj.first=John&name%2Eobj.last=Doe', - // ); - expect( - stringify( - { 'name.obj': { first: 'John', last: 'Doe' } }, - { - encodeDotInKeys: true, - allowDots: true, - encodeValuesOnly: true, - }, - ), - ).toBe('name%2Eobj.first=John&name%2Eobj.last=Doe'); - - // st.equal( - // stringify( - // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - // { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }, - // ), - // 'name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe', - // ); - expect( - stringify( - { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, - { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }, - ), - ).toBe('name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe'); - }); - - test('throws when `commaRoundTrip` is not a boolean', function () { - // st['throws']( - // function () { - // stringify({}, { commaRoundTrip: 'not a boolean' }); - // }, - // TypeError, - // 'throws when `commaRoundTrip` is not a boolean', - // ); - expect(() => { - // @ts-expect-error - stringify({}, { commaRoundTrip: 'not a boolean' }); - }).toThrow(TypeError); - }); - - test('throws when `encodeDotInKeys` is not a boolean', function () { - // st['throws'](function () { - // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); - // }, TypeError); - expect(() => { - // @ts-expect-error - stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); - }).toThrow(TypeError); - - // st['throws'](function () { - // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); - // }, TypeError); - expect(() => { - // @ts-expect-error - stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); - }).toThrow(TypeError); - - // st['throws'](function () { - // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); - // }, TypeError); - expect(() => { - // @ts-expect-error - stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); - }).toThrow(TypeError); - - // st['throws'](function () { - // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); - // }, TypeError); - expect(() => { - // @ts-expect-error - stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); - }).toThrow(TypeError); - }); - - test('adds query prefix', function () { - // st.equal(stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b'); - expect(stringify({ a: 'b' }, { addQueryPrefix: true })).toBe('?a=b'); - }); - - test('with query prefix, outputs blank string given an empty object', function () { - // st.equal(stringify({}, { addQueryPrefix: true }), ''); - expect(stringify({}, { addQueryPrefix: true })).toBe(''); - }); - - test('stringifies nested falsy values', function () { - // st.equal(stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D='); - // st.equal( - // stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), - // 'a%5Bb%5D%5Bc%5D', - // ); - // st.equal(stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false'); - expect(stringify({ a: { b: { c: null } } })).toBe('a%5Bb%5D%5Bc%5D='); - expect(stringify({ a: { b: { c: null } } }, { strictNullHandling: true })).toBe('a%5Bb%5D%5Bc%5D'); - expect(stringify({ a: { b: { c: false } } })).toBe('a%5Bb%5D%5Bc%5D=false'); - }); - - test('stringifies a nested object', function () { - // st.equal(stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); - // st.equal(stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e'); - expect(stringify({ a: { b: 'c' } })).toBe('a%5Bb%5D=c'); - expect(stringify({ a: { b: { c: { d: 'e' } } } })).toBe('a%5Bb%5D%5Bc%5D%5Bd%5D=e'); - }); - - test('`allowDots` option: stringifies a nested object with dots notation', function () { - // st.equal(stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c'); - // st.equal(stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e'); - expect(stringify({ a: { b: 'c' } }, { allowDots: true })).toBe('a.b=c'); - expect(stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true })).toBe('a.b.c.d=e'); - }); - - test('stringifies an array value', function () { - // st.equal( - // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }), - // 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', - // 'indices => indices', - // ); - // st.equal( - // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }), - // 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d', - // 'brackets => brackets', - // ); - // st.equal( - // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' }), - // 'a=b%2Cc%2Cd', - // 'comma => comma', - // ); - // st.equal( - // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true }), - // 'a=b%2Cc%2Cd', - // 'comma round trip => comma', - // ); - // st.equal( - // stringify({ a: ['b', 'c', 'd'] }), - // 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', - // 'default => indices', - // ); - expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' })).toBe( - 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', - ); - expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' })).toBe( - 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d', - ); - expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' })).toBe('a=b%2Cc%2Cd'); - expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true })).toBe( - 'a=b%2Cc%2Cd', - ); - expect(stringify({ a: ['b', 'c', 'd'] })).toBe('a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d'); - }); - - test('`skipNulls` option', function () { - // st.equal( - // stringify({ a: 'b', c: null }, { skipNulls: true }), - // 'a=b', - // 'omits nulls when asked', - // ); - expect(stringify({ a: 'b', c: null }, { skipNulls: true })).toBe('a=b'); - - // st.equal( - // stringify({ a: { b: 'c', d: null } }, { skipNulls: true }), - // 'a%5Bb%5D=c', - // 'omits nested nulls when asked', - // ); - expect(stringify({ a: { b: 'c', d: null } }, { skipNulls: true })).toBe('a%5Bb%5D=c'); - }); - - test('omits array indices when asked', function () { - // st.equal(stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d'); - expect(stringify({ a: ['b', 'c', 'd'] }, { indices: false })).toBe('a=b&a=c&a=d'); - }); - - test('omits object key/value pair when value is empty array', function () { - // st.equal(stringify({ a: [], b: 'zz' }), 'b=zz'); - expect(stringify({ a: [], b: 'zz' })).toBe('b=zz'); - }); - - test('should not omit object key/value pair when value is empty array and when asked', function () { - // st.equal(stringify({ a: [], b: 'zz' }), 'b=zz'); - // st.equal(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz'); - // st.equal(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz'); - expect(stringify({ a: [], b: 'zz' })).toBe('b=zz'); - expect(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false })).toBe('b=zz'); - expect(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true })).toBe('a[]&b=zz'); - }); - - test('should throw when allowEmptyArrays is not of type boolean', function () { - // st['throws'](function () { - // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); - // }, TypeError); - expect(() => { - // @ts-expect-error - stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); - }).toThrow(TypeError); - - // st['throws'](function () { - // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); - // }, TypeError); - expect(() => { - // @ts-expect-error - stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); - }).toThrow(TypeError); - - // st['throws'](function () { - // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); - // }, TypeError); - expect(() => { - // @ts-expect-error - stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); - }).toThrow(TypeError); - - // st['throws'](function () { - // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); - // }, TypeError); - expect(() => { - // @ts-expect-error - stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); - }).toThrow(TypeError); - }); - - test('allowEmptyArrays + strictNullHandling', function () { - // st.equal( - // stringify({ testEmptyArray: [] }, { strictNullHandling: true, allowEmptyArrays: true }), - // 'testEmptyArray[]', - // ); - expect(stringify({ testEmptyArray: [] }, { strictNullHandling: true, allowEmptyArrays: true })).toBe( - 'testEmptyArray[]', - ); - }); - - describe('stringifies an array value with one item vs multiple items', function () { - test('non-array item', function () { - // s2t.equal( - // stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - // 'a=c', - // ); - // s2t.equal( - // stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - // 'a=c', - // ); - // s2t.equal(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c'); - // s2t.equal(stringify({ a: 'c' }, { encodeValuesOnly: true }), 'a=c'); - expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe('a=c'); - expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a=c'); - expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c'); - expect(stringify({ a: 'c' }, { encodeValuesOnly: true })).toBe('a=c'); - }); - - test('array with a single item', function () { - // s2t.equal( - // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - // 'a[0]=c', - // ); - // s2t.equal( - // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - // 'a[]=c', - // ); - // s2t.equal( - // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), - // 'a=c', - // ); - // s2t.equal( - // stringify( - // { a: ['c'] }, - // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, - // ), - // 'a[]=c', - // ); // so it parses back as an array - // s2t.equal(stringify({ a: ['c'] }, { encodeValuesOnly: true }), 'a[0]=c'); - expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe('a[0]=c'); - expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a[]=c'); - expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c'); - expect( - stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), - ).toBe('a[]=c'); - expect(stringify({ a: ['c'] }, { encodeValuesOnly: true })).toBe('a[0]=c'); - }); - - test('array with multiple items', function () { - // s2t.equal( - // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - // 'a[0]=c&a[1]=d', - // ); - // s2t.equal( - // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - // 'a[]=c&a[]=d', - // ); - // s2t.equal( - // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), - // 'a=c,d', - // ); - // s2t.equal( - // stringify( - // { a: ['c', 'd'] }, - // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, - // ), - // 'a=c,d', - // ); - // s2t.equal(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true }), 'a[0]=c&a[1]=d'); - expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( - 'a[0]=c&a[1]=d', - ); - expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( - 'a[]=c&a[]=d', - ); - expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c,d'); - expect( - stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), - ).toBe('a=c,d'); - expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true })).toBe('a[0]=c&a[1]=d'); - }); - - test('array with multiple items with a comma inside', function () { - // s2t.equal( - // stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), - // 'a=c%2Cd,e', - // ); - // s2t.equal(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' }), 'a=c%2Cd%2Ce'); - expect(stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe( - 'a=c%2Cd,e', - ); - expect(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' })).toBe('a=c%2Cd%2Ce'); - - // s2t.equal( - // stringify( - // { a: ['c,d', 'e'] }, - // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, - // ), - // 'a=c%2Cd,e', - // ); - // s2t.equal( - // stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true }), - // 'a=c%2Cd%2Ce', - // ); - expect( - stringify( - { a: ['c,d', 'e'] }, - { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, - ), - ).toBe('a=c%2Cd,e'); - expect(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true })).toBe( - 'a=c%2Cd%2Ce', - ); - }); - }); - - test('stringifies a nested array value', function () { - expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( - 'a[b][0]=c&a[b][1]=d', - ); - expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( - 'a[b][]=c&a[b][]=d', - ); - expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe( - 'a[b]=c,d', - ); - expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true })).toBe('a[b][0]=c&a[b][1]=d'); - }); - - test('stringifies comma and empty array values', function () { - // st.equal( - // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' }), - // 'a[0]=,&a[1]=&a[2]=c,d%', - // ); - // st.equal( - // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' }), - // 'a[]=,&a[]=&a[]=c,d%', - // ); - // st.equal( - // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' }), - // 'a=,,,c,d%', - // ); - // st.equal( - // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' }), - // 'a=,&a=&a=c,d%', - // ); - expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' })).toBe( - 'a[0]=,&a[1]=&a[2]=c,d%', - ); - expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' })).toBe( - 'a[]=,&a[]=&a[]=c,d%', - ); - expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' })).toBe('a=,,,c,d%'); - expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' })).toBe( - 'a=,&a=&a=c,d%', - ); - - // st.equal( - // stringify( - // { a: [',', '', 'c,d%'] }, - // { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, - // ), - // 'a[0]=%2C&a[1]=&a[2]=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: [',', '', 'c,d%'] }, - // { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, - // ), - // 'a[]=%2C&a[]=&a[]=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: [',', '', 'c,d%'] }, - // { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }, - // ), - // 'a=%2C,,c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: [',', '', 'c,d%'] }, - // { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, - // ), - // 'a=%2C&a=&a=c%2Cd%25', - // ); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), - ).toBe('a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), - ).toBe('a[]=%2C&a[]=&a[]=c%2Cd%25'); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), - ).toBe('a=%2C%2C%2Cc%2Cd%25'); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), - ).toBe('a=%2C&a=&a=c%2Cd%25'); - - // st.equal( - // stringify( - // { a: [',', '', 'c,d%'] }, - // { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, - // ), - // 'a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: [',', '', 'c,d%'] }, - // { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, - // ), - // 'a%5B%5D=%2C&a%5B%5D=&a%5B%5D=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: [',', '', 'c,d%'] }, - // { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, - // ), - // 'a=%2C%2C%2Cc%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: [',', '', 'c,d%'] }, - // { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, - // ), - // 'a=%2C&a=&a=c%2Cd%25', - // ); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), - ).toBe('a=%2C&a=&a=c%2Cd%25'); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), - ).toBe('a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), - ).toBe('a[]=%2C&a[]=&a[]=c%2Cd%25'); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), - ).toBe('a=%2C%2C%2Cc%2Cd%25'); - expect( - stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), - ).toBe('a=%2C&a=&a=c%2Cd%25'); - }); - - test('stringifies comma and empty non-array values', function () { - // st.equal( - // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), - // 'a=,&b=&c=c,d%', - // ); - // st.equal( - // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), - // 'a=,&b=&c=c,d%', - // ); - // st.equal( - // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'comma' }), - // 'a=,&b=&c=c,d%', - // ); - // st.equal( - // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'repeat' }), - // 'a=,&b=&c=c,d%', - // ); - expect(stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' })).toBe( - 'a=,&b=&c=c,d%', - ); - expect(stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' })).toBe( - 'a=,&b=&c=c,d%', - ); - - // st.equal( - // stringify( - // { a: ',', b: '', c: 'c,d%' }, - // { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, - // ), - // 'a=%2C&b=&c=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: ',', b: '', c: 'c,d%' }, - // { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, - // ), - // 'a=%2C&b=&c=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: ',', b: '', c: 'c,d%' }, - // { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }, - // ), - // 'a=%2C&b=&c=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: ',', b: '', c: 'c,d%' }, - // { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, - // ), - // 'a=%2C&b=&c=c%2Cd%25', - // ); - expect( - stringify( - { a: ',', b: '', c: 'c,d%' }, - { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, - ), - ).toBe('a=%2C&b=&c=c%2Cd%25'); - expect( - stringify( - { a: ',', b: '', c: 'c,d%' }, - { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, - ), - ).toBe('a=%2C&b=&c=c%2Cd%25'); - expect( - stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), - ).toBe('a=%2C&b=&c=c%2Cd%25'); - expect( - stringify( - { a: ',', b: '', c: 'c,d%' }, - { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, - ), - ).toBe('a=%2C&b=&c=c%2Cd%25'); - - // st.equal( - // stringify( - // { a: ',', b: '', c: 'c,d%' }, - // { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, - // ), - // 'a=%2C&b=&c=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: ',', b: '', c: 'c,d%' }, - // { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, - // ), - // 'a=%2C&b=&c=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: ',', b: '', c: 'c,d%' }, - // { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, - // ), - // 'a=%2C&b=&c=c%2Cd%25', - // ); - // st.equal( - // stringify( - // { a: ',', b: '', c: 'c,d%' }, - // { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, - // ), - // 'a=%2C&b=&c=c%2Cd%25', - // ); - expect( - stringify( - { a: ',', b: '', c: 'c,d%' }, - { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, - ), - ).toBe('a=%2C&b=&c=c%2Cd%25'); - expect( - stringify( - { a: ',', b: '', c: 'c,d%' }, - { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, - ), - ).toBe('a=%2C&b=&c=c%2Cd%25'); - expect( - stringify( - { a: ',', b: '', c: 'c,d%' }, - { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, - ), - ).toBe('a=%2C&b=&c=c%2Cd%25'); - expect( - stringify( - { a: ',', b: '', c: 'c,d%' }, - { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, - ), - ).toBe('a=%2C&b=&c=c%2Cd%25'); - }); - - test('stringifies a nested array value with dots notation', function () { - // st.equal( - // stringify( - // { a: { b: ['c', 'd'] } }, - // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }, - // ), - // 'a.b[0]=c&a.b[1]=d', - // 'indices: stringifies with dots + indices', - // ); - // st.equal( - // stringify( - // { a: { b: ['c', 'd'] } }, - // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, - // ), - // 'a.b[]=c&a.b[]=d', - // 'brackets: stringifies with dots + brackets', - // ); - // st.equal( - // stringify( - // { a: { b: ['c', 'd'] } }, - // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }, - // ), - // 'a.b=c,d', - // 'comma: stringifies with dots + comma', - // ); - // st.equal( - // stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true }), - // 'a.b[0]=c&a.b[1]=d', - // 'default: stringifies with dots + indices', - // ); - expect( - stringify( - { a: { b: ['c', 'd'] } }, - { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }, - ), - ).toBe('a.b[0]=c&a.b[1]=d'); - expect( - stringify( - { a: { b: ['c', 'd'] } }, - { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, - ), - ).toBe('a.b[]=c&a.b[]=d'); - expect( - stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }), - ).toBe('a.b=c,d'); - expect(stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true })).toBe( - 'a.b[0]=c&a.b[1]=d', - ); - }); - - test('stringifies an object inside an array', function () { - // st.equal( - // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true }), - // 'a[0][b]=c', - // 'indices => indices', - // ); - // st.equal( - // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }), - // 'a[b]=c', - // 'repeat => repeat', - // ); - // st.equal( - // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }), - // 'a[][b]=c', - // 'brackets => brackets', - // ); - // st.equal( - // stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true }), - // 'a[0][b]=c', - // 'default => indices', - // ); - expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true })).toBe( - 'a[0][b]=c', - ); - expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true })).toBe('a[b]=c'); - expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true })).toBe( - 'a[][b]=c', - ); - expect(stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true })).toBe('a[0][b]=c'); - - // st.equal( - // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true }), - // 'a[0][b][c][0]=1', - // 'indices => indices', - // ); - // st.equal( - // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }), - // 'a[b][c]=1', - // 'repeat => repeat', - // ); - // st.equal( - // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }), - // 'a[][b][c][]=1', - // 'brackets => brackets', - // ); - // st.equal( - // stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true }), - // 'a[0][b][c][0]=1', - // 'default => indices', - // ); - expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true })).toBe( - 'a[0][b][c][0]=1', - ); - expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true })).toBe( - 'a[b][c]=1', - ); - expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true })).toBe( - 'a[][b][c][]=1', - ); - expect(stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true })).toBe('a[0][b][c][0]=1'); - }); - - test('stringifies an array with mixed objects and primitives', function () { - // st.equal( - // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - // 'a[0][b]=1&a[1]=2&a[2]=3', - // 'indices => indices', - // ); - // st.equal( - // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - // 'a[][b]=1&a[]=2&a[]=3', - // 'brackets => brackets', - // ); - // st.equal( - // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), - // '???', - // 'brackets => brackets', - // { skip: 'TODO: figure out what this should do' }, - // ); - // st.equal( - // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true }), - // 'a[0][b]=1&a[1]=2&a[2]=3', - // 'default => indices', - // ); - expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( - 'a[0][b]=1&a[1]=2&a[2]=3', - ); - expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( - 'a[][b]=1&a[]=2&a[]=3', - ); - // !Skipped: Figure out what this should do - // expect( - // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), - // ).toBe('???'); - expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true })).toBe('a[0][b]=1&a[1]=2&a[2]=3'); - }); - - test('stringifies an object inside an array with dots notation', function () { - // st.equal( - // stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'indices' }), - // 'a[0].b=c', - // 'indices => indices', - // ); - // st.equal( - // stringify( - // { a: [{ b: 'c' }] }, - // { allowDots: true, encode: false, arrayFormat: 'brackets' }, - // ), - // 'a[].b=c', - // 'brackets => brackets', - // ); - // st.equal( - // stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false }), - // 'a[0].b=c', - // 'default => indices', - // ); - expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'indices' })).toBe( - 'a[0].b=c', - ); - expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'brackets' })).toBe( - 'a[].b=c', - ); - expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false })).toBe('a[0].b=c'); - - // st.equal( - // stringify( - // { a: [{ b: { c: [1] } }] }, - // { allowDots: true, encode: false, arrayFormat: 'indices' }, - // ), - // 'a[0].b.c[0]=1', - // 'indices => indices', - // ); - // st.equal( - // stringify( - // { a: [{ b: { c: [1] } }] }, - // { allowDots: true, encode: false, arrayFormat: 'brackets' }, - // ), - // 'a[].b.c[]=1', - // 'brackets => brackets', - // ); - // st.equal( - // stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false }), - // 'a[0].b.c[0]=1', - // 'default => indices', - // ); - expect( - stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false, arrayFormat: 'indices' }), - ).toBe('a[0].b.c[0]=1'); - expect( - stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false, arrayFormat: 'brackets' }), - ).toBe('a[].b.c[]=1'); - expect(stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false })).toBe('a[0].b.c[0]=1'); - }); - - test('does not omit object keys when indices = false', function () { - // st.equal(stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c'); - expect(stringify({ a: [{ b: 'c' }] }, { indices: false })).toBe('a%5Bb%5D=c'); - }); - - test('uses indices notation for arrays when indices=true', function () { - // st.equal(stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c'); - expect(stringify({ a: ['b', 'c'] }, { indices: true })).toBe('a%5B0%5D=b&a%5B1%5D=c'); - }); - - test('uses indices notation for arrays when no arrayFormat is specified', function () { - // st.equal(stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c'); - expect(stringify({ a: ['b', 'c'] })).toBe('a%5B0%5D=b&a%5B1%5D=c'); - }); - - test('uses indices notation for arrays when arrayFormat=indices', function () { - // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c'); - expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })).toBe('a%5B0%5D=b&a%5B1%5D=c'); - }); - - test('uses repeat notation for arrays when arrayFormat=repeat', function () { - // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c'); - expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })).toBe('a=b&a=c'); - }); - - test('uses brackets notation for arrays when arrayFormat=brackets', function () { - // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c'); - expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })).toBe('a%5B%5D=b&a%5B%5D=c'); - }); - - test('stringifies a complicated object', function () { - // st.equal(stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e'); - expect(stringify({ a: { b: 'c', d: 'e' } })).toBe('a%5Bb%5D=c&a%5Bd%5D=e'); - }); - - test('stringifies an empty value', function () { - // st.equal(stringify({ a: '' }), 'a='); - // st.equal(stringify({ a: null }, { strictNullHandling: true }), 'a'); - expect(stringify({ a: '' })).toBe('a='); - expect(stringify({ a: null }, { strictNullHandling: true })).toBe('a'); - - // st.equal(stringify({ a: '', b: '' }), 'a=&b='); - // st.equal(stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b='); - expect(stringify({ a: '', b: '' })).toBe('a=&b='); - expect(stringify({ a: null, b: '' }, { strictNullHandling: true })).toBe('a&b='); - - // st.equal(stringify({ a: { b: '' } }), 'a%5Bb%5D='); - // st.equal(stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D'); - // st.equal(stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D='); - expect(stringify({ a: { b: '' } })).toBe('a%5Bb%5D='); - expect(stringify({ a: { b: null } }, { strictNullHandling: true })).toBe('a%5Bb%5D'); - expect(stringify({ a: { b: null } }, { strictNullHandling: false })).toBe('a%5Bb%5D='); - }); - - test('stringifies an empty array in different arrayFormat', function () { - // st.equal(stringify({ a: [], b: [null], c: 'c' }, { encode: false }), 'b[0]=&c=c'); - expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false })).toBe('b[0]=&c=c'); - // arrayFormat default - // st.equal( - // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' }), - // 'b[0]=&c=c', - // ); - // st.equal( - // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' }), - // 'b[]=&c=c', - // ); - // st.equal( - // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' }), - // 'b=&c=c', - // ); - // st.equal( - // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' }), - // 'b=&c=c', - // ); - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'comma', commaRoundTrip: true }, - // ), - // 'b[]=&c=c', - // ); - expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' })).toBe( - 'b[0]=&c=c', - ); - expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' })).toBe( - 'b[]=&c=c', - ); - expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' })).toBe('b=&c=c'); - expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' })).toBe('b=&c=c'); - expect( - stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', commaRoundTrip: true }), - ).toBe('b[]=&c=c'); - - // with strictNullHandling - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'indices', strictNullHandling: true }, - // ), - // 'b[0]&c=c', - // ); - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'brackets', strictNullHandling: true }, - // ), - // 'b[]&c=c', - // ); - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'repeat', strictNullHandling: true }, - // ), - // 'b&c=c', - // ); - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'comma', strictNullHandling: true }, - // ), - // 'b&c=c', - // ); - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }, - // ), - // 'b[]&c=c', - // ); - - expect( - stringify( - { a: [], b: [null], c: 'c' }, - { encode: false, arrayFormat: 'indices', strictNullHandling: true }, - ), - ).toBe('b[0]&c=c'); - expect( - stringify( - { a: [], b: [null], c: 'c' }, - { encode: false, arrayFormat: 'brackets', strictNullHandling: true }, - ), - ).toBe('b[]&c=c'); - expect( - stringify( - { a: [], b: [null], c: 'c' }, - { encode: false, arrayFormat: 'repeat', strictNullHandling: true }, - ), - ).toBe('b&c=c'); - expect( - stringify( - { a: [], b: [null], c: 'c' }, - { encode: false, arrayFormat: 'comma', strictNullHandling: true }, - ), - ).toBe('b&c=c'); - expect( - stringify( - { a: [], b: [null], c: 'c' }, - { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }, - ), - ).toBe('b[]&c=c'); - - // with skipNulls - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'indices', skipNulls: true }, - // ), - // 'c=c', - // ); - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'brackets', skipNulls: true }, - // ), - // 'c=c', - // ); - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'repeat', skipNulls: true }, - // ), - // 'c=c', - // ); - // st.equal( - // stringify( - // { a: [], b: [null], c: 'c' }, - // { encode: false, arrayFormat: 'comma', skipNulls: true }, - // ), - // 'c=c', - // ); - expect( - stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', skipNulls: true }), - ).toBe('c=c'); - expect( - stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', skipNulls: true }), - ).toBe('c=c'); - expect( - stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', skipNulls: true }), - ).toBe('c=c'); - expect( - stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', skipNulls: true }), - ).toBe('c=c'); - }); - - test('stringifies a null object', function () { - var obj = Object.create(null); - obj.a = 'b'; - // st.equal(stringify(obj), 'a=b'); - expect(stringify(obj)).toBe('a=b'); - }); - - test('returns an empty string for invalid input', function () { - // st.equal(stringify(undefined), ''); - // st.equal(stringify(false), ''); - // st.equal(stringify(null), ''); - // st.equal(stringify(''), ''); - expect(stringify(undefined)).toBe(''); - expect(stringify(false)).toBe(''); - expect(stringify(null)).toBe(''); - expect(stringify('')).toBe(''); - }); - - test('stringifies an object with a null object as a child', function () { - var obj = { a: Object.create(null) }; - - obj.a.b = 'c'; - // st.equal(stringify(obj), 'a%5Bb%5D=c'); - expect(stringify(obj)).toBe('a%5Bb%5D=c'); - }); - - test('drops keys with a value of undefined', function () { - // st.equal(stringify({ a: undefined }), ''); - expect(stringify({ a: undefined })).toBe(''); - - // st.equal( - // stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), - // 'a%5Bc%5D', - // ); - // st.equal( - // stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), - // 'a%5Bc%5D=', - // ); - // st.equal(stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D='); - expect(stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true })).toBe('a%5Bc%5D'); - expect(stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false })).toBe('a%5Bc%5D='); - expect(stringify({ a: { b: undefined, c: '' } })).toBe('a%5Bc%5D='); - }); - - test('url encodes values', function () { - // st.equal(stringify({ a: 'b c' }), 'a=b%20c'); - expect(stringify({ a: 'b c' })).toBe('a=b%20c'); - }); - - test('stringifies a date', function () { - var now = new Date(); - var str = 'a=' + encodeURIComponent(now.toISOString()); - // st.equal(stringify({ a: now }), str); - expect(stringify({ a: now })).toBe(str); - }); - - test('stringifies the weird object from qs', function () { - // st.equal( - // stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), - // 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F', - // ); - expect(stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' })).toBe( - 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F', - ); - }); - - // TODO: Investigate how to to intercept in vitest - // TODO(rob) - test('skips properties that are part of the object prototype', function () { - // st.intercept(Object.prototype, 'crash', { value: 'test' }); - // @ts-expect-error - Object.prototype.crash = 'test'; - - // st.equal(stringify({ a: 'b' }), 'a=b'); - // st.equal(stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); - expect(stringify({ a: 'b' })).toBe('a=b'); - expect(stringify({ a: { b: 'c' } })).toBe('a%5Bb%5D=c'); - }); - - test('stringifies boolean values', function () { - // st.equal(stringify({ a: true }), 'a=true'); - // st.equal(stringify({ a: { b: true } }), 'a%5Bb%5D=true'); - // st.equal(stringify({ b: false }), 'b=false'); - // st.equal(stringify({ b: { c: false } }), 'b%5Bc%5D=false'); - expect(stringify({ a: true })).toBe('a=true'); - expect(stringify({ a: { b: true } })).toBe('a%5Bb%5D=true'); - expect(stringify({ b: false })).toBe('b=false'); - expect(stringify({ b: { c: false } })).toBe('b%5Bc%5D=false'); - }); - - test('stringifies buffer values', function () { - // st.equal(stringify({ a: Buffer.from('test') }), 'a=test'); - // st.equal(stringify({ a: { b: Buffer.from('test') } }), 'a%5Bb%5D=test'); - }); - - test('stringifies an object using an alternative delimiter', function () { - // st.equal(stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d'); - expect(stringify({ a: 'b', c: 'd' }, { delimiter: ';' })).toBe('a=b;c=d'); - }); - - // We dont target environments which dont even have Buffer - // test('does not blow up when Buffer global is missing', function () { - // var restore = mockProperty(global, 'Buffer', { delete: true }); - - // var result = stringify({ a: 'b', c: 'd' }); - - // restore(); - - // st.equal(result, 'a=b&c=d'); - // st.end(); - // }); - - test('does not crash when parsing circular references', function () { - var a: any = {}; - a.b = a; - - // st['throws']( - // function () { - // stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); - // }, - // /RangeError: Cyclic object value/, - // 'cyclic values throw', - // ); - expect(() => { - stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); - }).toThrow('Cyclic object value'); - - var circular: any = { - a: 'value', - }; - circular.a = circular; - // st['throws']( - // function () { - // stringify(circular); - // }, - // /RangeError: Cyclic object value/, - // 'cyclic values throw', - // ); - expect(() => { - stringify(circular); - }).toThrow('Cyclic object value'); - - var arr = ['a']; - // st.doesNotThrow(function () { - // stringify({ x: arr, y: arr }); - // }, 'non-cyclic values do not throw'); - expect(() => { - stringify({ x: arr, y: arr }); - }).not.toThrow(); - }); - - test('non-circular duplicated references can still work', function () { - var hourOfDay = { - function: 'hour_of_day', - }; - - var p1 = { - function: 'gte', - arguments: [hourOfDay, 0], - }; - var p2 = { - function: 'lte', - arguments: [hourOfDay, 23], - }; - - // st.equal( - // stringify( - // { filters: { $and: [p1, p2] } }, - // { encodeValuesOnly: true, arrayFormat: 'indices' }, - // ), - // 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23', - // ); - // st.equal( - // stringify( - // { filters: { $and: [p1, p2] } }, - // { encodeValuesOnly: true, arrayFormat: 'brackets' }, - // ), - // 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23', - // ); - // st.equal( - // stringify( - // { filters: { $and: [p1, p2] } }, - // { encodeValuesOnly: true, arrayFormat: 'repeat' }, - // ), - // 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23', - // ); - expect( - stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - ).toBe( - 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23', - ); - expect( - stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - ).toBe( - 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23', - ); - expect( - stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), - ).toBe( - 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23', - ); - }); - - test('selects properties when filter=array', function () { - // st.equal(stringify({ a: 'b' }, { filter: ['a'] }), 'a=b'); - // st.equal(stringify({ a: 1 }, { filter: [] }), ''); - expect(stringify({ a: 'b' }, { filter: ['a'] })).toBe('a=b'); - expect(stringify({ a: 1 }, { filter: [] })).toBe(''); - - // st.equal( - // stringify( - // { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, - // { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }, - // ), - // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', - // 'indices => indices', - // ); - // st.equal( - // stringify( - // { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, - // { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }, - // ), - // 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3', - // 'brackets => brackets', - // ); - // st.equal( - // stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] }), - // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', - // 'default => indices', - // ); - expect(stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] })).toBe( - 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', - ); - expect( - stringify( - { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, - { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }, - ), - ).toBe('a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3'); - expect( - stringify( - { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, - { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }, - ), - ).toBe('a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3'); - }); - - test('supports custom representations when filter=function', function () { - var calls = 0; - var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } }; - var filterFunc: StringifyOptions['filter'] = function (prefix, value) { - calls += 1; - if (calls === 1) { - // st.equal(prefix, '', 'prefix is empty'); - // st.equal(value, obj); - expect(prefix).toBe(''); - expect(value).toBe(obj); - } else if (prefix === 'c') { - return void 0; - } else if (value instanceof Date) { - // st.equal(prefix, 'e[f]'); - expect(prefix).toBe('e[f]'); - return value.getTime(); - } - return value; - }; - - // st.equal(stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000'); - // st.equal(calls, 5); - expect(stringify(obj, { filter: filterFunc })).toBe('a=b&e%5Bf%5D=1257894000000'); - expect(calls).toBe(5); - }); - - test('can disable uri encoding', function () { - // st.equal(stringify({ a: 'b' }, { encode: false }), 'a=b'); - // st.equal(stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c'); - // st.equal( - // stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), - // 'a=b&c', - // ); - expect(stringify({ a: 'b' }, { encode: false })).toBe('a=b'); - expect(stringify({ a: { b: 'c' } }, { encode: false })).toBe('a[b]=c'); - expect(stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false })).toBe('a=b&c'); - }); - - test('can sort the keys', function () { - // @ts-expect-error - var sort: NonNullable = function (a: string, b: string) { - return a.localeCompare(b); - }; - // st.equal(stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y'); - // st.equal( - // stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), - // 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a', - // ); - expect(stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort })).toBe('a=c&b=f&z=y'); - expect(stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort })).toBe( - 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a', - ); - }); - - test('can sort the keys at depth 3 or more too', function () { - // @ts-expect-error - var sort: NonNullable = function (a: string, b: string) { - return a.localeCompare(b); - }; - // st.equal( - // stringify( - // { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, - // { sort: sort, encode: false }, - // ), - // 'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb', - // ); - // st.equal( - // stringify( - // { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, - // { sort: null, encode: false }, - // ), - // 'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b', - // ); - expect( - stringify( - { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, - { sort: sort, encode: false }, - ), - ).toBe('a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'); - expect( - stringify( - { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, - { sort: null, encode: false }, - ), - ).toBe('a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'); - }); - - test('can stringify with custom encoding', function () { - // st.equal( - // stringify( - // { 県: '大阪府', '': '' }, - // { - // encoder: function (str) { - // if (str.length === 0) { - // return ''; - // } - // var buf = iconv.encode(str, 'shiftjis'); - // var result = []; - // for (var i = 0; i < buf.length; ++i) { - // result.push(buf.readUInt8(i).toString(16)); - // } - // return '%' + result.join('%'); - // }, - // }, - // ), - // '%8c%a7=%91%e5%8d%e3%95%7b&=', - // ); - expect( - stringify( - { 県: '大阪府', '': '' }, - { - encoder: function (str) { - if (str.length === 0) { - return ''; - } - var buf = iconv.encode(str, 'shiftjis'); - var result = []; - for (var i = 0; i < buf.length; ++i) { - result.push(buf.readUInt8(i).toString(16)); - } - return '%' + result.join('%'); - }, - }, - ), - ).toBe('%8c%a7=%91%e5%8d%e3%95%7b&='); - }); - - test('receives the default encoder as a second argument', function () { - // stringify( - // { a: 1, b: new Date(), c: true, d: [1] }, - // { - // encoder: function (str) { - // st.match(typeof str, /^(?:string|number|boolean)$/); - // return ''; - // }, - // }, - // ); - - stringify( - { a: 1, b: new Date(), c: true, d: [1] }, - { - encoder: function (str) { - // st.match(typeof str, /^(?:string|number|boolean)$/); - assert.match(typeof str, /^(?:string|number|boolean)$/); - return ''; - }, - }, - ); - }); - - test('receives the default encoder as a second argument', function () { - // stringify( - // { a: 1 }, - // { - // encoder: function (str, defaultEncoder) { - // st.equal(defaultEncoder, utils.encode); - // }, - // }, - // ); - - stringify( - { a: 1 }, - { - // @ts-ignore - encoder: function (_str, defaultEncoder) { - expect(defaultEncoder).toBe(encode); - }, - }, - ); - }); - - test('throws error with wrong encoder', function () { - // st['throws'](function () { - // stringify({}, { encoder: 'string' }); - // }, new TypeError('Encoder has to be a function.')); - // st.end(); - expect(() => { - // @ts-expect-error - stringify({}, { encoder: 'string' }); - }).toThrow(TypeError); - }); - - (typeof Buffer === 'undefined' ? test.skip : test)( - 'can use custom encoder for a buffer object', - function () { - // st.equal( - // stringify( - // { a: Buffer.from([1]) }, - // { - // encoder: function (buffer) { - // if (typeof buffer === 'string') { - // return buffer; - // } - // return String.fromCharCode(buffer.readUInt8(0) + 97); - // }, - // }, - // ), - // 'a=b', - // ); - expect( - stringify( - { a: Buffer.from([1]) }, - { - encoder: function (buffer) { - if (typeof buffer === 'string') { - return buffer; - } - return String.fromCharCode(buffer.readUInt8(0) + 97); - }, - }, - ), - ).toBe('a=b'); - - // st.equal( - // stringify( - // { a: Buffer.from('a b') }, - // { - // encoder: function (buffer) { - // return buffer; - // }, - // }, - // ), - // 'a=a b', - // ); - expect( - stringify( - { a: Buffer.from('a b') }, - { - encoder: function (buffer) { - return buffer; - }, - }, - ), - ).toBe('a=a b'); - }, - ); - - test('serializeDate option', function () { - var date = new Date(); - // st.equal( - // stringify({ a: date }), - // 'a=' + date.toISOString().replace(/:/g, '%3A'), - // 'default is toISOString', - // ); - expect(stringify({ a: date })).toBe('a=' + date.toISOString().replace(/:/g, '%3A')); - - var mutatedDate = new Date(); - mutatedDate.toISOString = function () { - throw new SyntaxError(); - }; - // st['throws'](function () { - // mutatedDate.toISOString(); - // }, SyntaxError); - expect(() => { - mutatedDate.toISOString(); - }).toThrow(SyntaxError); - // st.equal( - // stringify({ a: mutatedDate }), - // 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'), - // 'toISOString works even when method is not locally present', - // ); - expect(stringify({ a: mutatedDate })).toBe( - 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'), - ); - - var specificDate = new Date(6); - // st.equal( - // stringify( - // { a: specificDate }, - // { - // serializeDate: function (d) { - // return d.getTime() * 7; - // }, - // }, - // ), - // 'a=42', - // 'custom serializeDate function called', - // ); - expect( - stringify( - { a: specificDate }, - { - // @ts-ignore - serializeDate: function (d) { - return d.getTime() * 7; - }, - }, - ), - ).toBe('a=42'); - - // st.equal( - // stringify( - // { a: [date] }, - // { - // serializeDate: function (d) { - // return d.getTime(); - // }, - // arrayFormat: 'comma', - // }, - // ), - // 'a=' + date.getTime(), - // 'works with arrayFormat comma', - // ); - // st.equal( - // stringify( - // { a: [date] }, - // { - // serializeDate: function (d) { - // return d.getTime(); - // }, - // arrayFormat: 'comma', - // commaRoundTrip: true, - // }, - // ), - // 'a%5B%5D=' + date.getTime(), - // 'works with arrayFormat comma', - // ); - expect( - stringify( - { a: [date] }, - { - // @ts-expect-error - serializeDate: function (d) { - return d.getTime(); - }, - arrayFormat: 'comma', - }, - ), - ).toBe('a=' + date.getTime()); - expect( - stringify( - { a: [date] }, - { - // @ts-expect-error - serializeDate: function (d) { - return d.getTime(); - }, - arrayFormat: 'comma', - commaRoundTrip: true, - }, - ), - ).toBe('a%5B%5D=' + date.getTime()); - }); - - test('RFC 1738 serialization', function () { - // st.equal(stringify({ a: 'b c' }, { format: formats.RFC1738 }), 'a=b+c'); - // st.equal(stringify({ 'a b': 'c d' }, { format: formats.RFC1738 }), 'a+b=c+d'); - // st.equal( - // stringify({ 'a b': Buffer.from('a b') }, { format: formats.RFC1738 }), - // 'a+b=a+b', - // ); - expect(stringify({ a: 'b c' }, { format: 'RFC1738' })).toBe('a=b+c'); - expect(stringify({ 'a b': 'c d' }, { format: 'RFC1738' })).toBe('a+b=c+d'); - expect(stringify({ 'a b': Buffer.from('a b') }, { format: 'RFC1738' })).toBe('a+b=a+b'); - - // st.equal(stringify({ 'foo(ref)': 'bar' }, { format: formats.RFC1738 }), 'foo(ref)=bar'); - expect(stringify({ 'foo(ref)': 'bar' }, { format: 'RFC1738' })).toBe('foo(ref)=bar'); - }); - - test('RFC 3986 spaces serialization', function () { - // st.equal(stringify({ a: 'b c' }, { format: formats.RFC3986 }), 'a=b%20c'); - // st.equal(stringify({ 'a b': 'c d' }, { format: formats.RFC3986 }), 'a%20b=c%20d'); - // st.equal( - // stringify({ 'a b': Buffer.from('a b') }, { format: formats.RFC3986 }), - // 'a%20b=a%20b', - // ); - expect(stringify({ a: 'b c' }, { format: 'RFC3986' })).toBe('a=b%20c'); - expect(stringify({ 'a b': 'c d' }, { format: 'RFC3986' })).toBe('a%20b=c%20d'); - expect(stringify({ 'a b': Buffer.from('a b') }, { format: 'RFC3986' })).toBe('a%20b=a%20b'); - }); - - test('Backward compatibility to RFC 3986', function () { - // st.equal(stringify({ a: 'b c' }), 'a=b%20c'); - // st.equal(stringify({ 'a b': Buffer.from('a b') }), 'a%20b=a%20b'); - expect(stringify({ a: 'b c' })).toBe('a=b%20c'); - expect(stringify({ 'a b': Buffer.from('a b') })).toBe('a%20b=a%20b'); - }); - - test('Edge cases and unknown formats', function () { - ['UFO1234', false, 1234, null, {}, []].forEach(function (format) { - // st['throws'](function () { - // stringify({ a: 'b c' }, { format: format }); - // }, new TypeError('Unknown format option provided.')); - expect(() => { - // @ts-expect-error - stringify({ a: 'b c' }, { format: format }); - }).toThrow(TypeError); - }); - }); - - test('encodeValuesOnly', function () { - // st.equal( - // stringify( - // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, - // { encodeValuesOnly: true, arrayFormat: 'indices' }, - // ), - // 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h', - // 'encodeValuesOnly + indices', - // ); - // st.equal( - // stringify( - // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, - // { encodeValuesOnly: true, arrayFormat: 'brackets' }, - // ), - // 'a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h', - // 'encodeValuesOnly + brackets', - // ); - // st.equal( - // stringify( - // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, - // { encodeValuesOnly: true, arrayFormat: 'repeat' }, - // ), - // 'a=b&c=d&c=e%3Df&f=g&f=h', - // 'encodeValuesOnly + repeat', - // ); - expect( - stringify( - { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, - { encodeValuesOnly: true, arrayFormat: 'indices' }, - ), - ).toBe('a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'); - expect( - stringify( - { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, - { encodeValuesOnly: true, arrayFormat: 'brackets' }, - ), - ).toBe('a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h'); - expect( - stringify( - { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, - { encodeValuesOnly: true, arrayFormat: 'repeat' }, - ), - ).toBe('a=b&c=d&c=e%3Df&f=g&f=h'); - - // st.equal( - // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'indices' }), - // 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h', - // 'no encodeValuesOnly + indices', - // ); - // st.equal( - // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'brackets' }), - // 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h', - // 'no encodeValuesOnly + brackets', - // ); - // st.equal( - // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'repeat' }), - // 'a=b&c=d&c=e&f=g&f=h', - // 'no encodeValuesOnly + repeat', - // ); - expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'indices' })).toBe( - 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h', - ); - expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'brackets' })).toBe( - 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h', - ); - expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'repeat' })).toBe( - 'a=b&c=d&c=e&f=g&f=h', - ); - }); - - test('encodeValuesOnly - strictNullHandling', function () { - // st.equal( - // stringify({ a: { b: null } }, { encodeValuesOnly: true, strictNullHandling: true }), - // 'a[b]', - // ); - expect(stringify({ a: { b: null } }, { encodeValuesOnly: true, strictNullHandling: true })).toBe('a[b]'); - }); - - test('throws if an invalid charset is specified', function () { - // st['throws'](function () { - // stringify({ a: 'b' }, { charset: 'foobar' }); - // }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined')); - expect(() => { - // @ts-expect-error - stringify({ a: 'b' }, { charset: 'foobar' }); - }).toThrow(TypeError); - }); - - test('respects a charset of iso-8859-1', function () { - // st.equal(stringify({ æ: 'æ' }, { charset: 'iso-8859-1' }), '%E6=%E6'); - expect(stringify({ æ: 'æ' }, { charset: 'iso-8859-1' })).toBe('%E6=%E6'); - }); - - test('encodes unrepresentable chars as numeric entities in iso-8859-1 mode', function () { - // st.equal(stringify({ a: '☺' }, { charset: 'iso-8859-1' }), 'a=%26%239786%3B'); - expect(stringify({ a: '☺' }, { charset: 'iso-8859-1' })).toBe('a=%26%239786%3B'); - }); - - test('respects an explicit charset of utf-8 (the default)', function () { - // st.equal(stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6'); - expect(stringify({ a: 'æ' }, { charset: 'utf-8' })).toBe('a=%C3%A6'); - }); - - test('`charsetSentinel` option', function () { - // st.equal( - // stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }), - // 'utf8=%E2%9C%93&a=%C3%A6', - // 'adds the right sentinel when instructed to and the charset is utf-8', - // ); - expect(stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' })).toBe( - 'utf8=%E2%9C%93&a=%C3%A6', - ); - - // st.equal( - // stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }), - // 'utf8=%26%2310003%3B&a=%E6', - // 'adds the right sentinel when instructed to and the charset is iso-8859-1', - // ); - expect(stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' })).toBe( - 'utf8=%26%2310003%3B&a=%E6', - ); - }); - - test('does not mutate the options argument', function () { - var options = {}; - stringify({}, options); - // st.deepEqual(options, {}); - expect(options).toEqual({}); - }); - - test('strictNullHandling works with custom filter', function () { - // @ts-expect-error - var filter = function (_prefix, value) { - return value; - }; - - var options = { strictNullHandling: true, filter: filter }; - // st.equal(stringify({ key: null }, options), 'key'); - expect(stringify({ key: null }, options)).toBe('key'); - }); - - test('strictNullHandling works with null serializeDate', function () { - var serializeDate = function () { - return null; - }; - var options = { strictNullHandling: true, serializeDate: serializeDate }; - var date = new Date(); - // st.equal(stringify({ key: date }, options), 'key'); - // @ts-expect-error - expect(stringify({ key: date }, options)).toBe('key'); - }); - - test('allows for encoding keys and values differently', function () { - // @ts-expect-error - var encoder = function (str, defaultEncoder, charset, type) { - if (type === 'key') { - return defaultEncoder(str, defaultEncoder, charset, type).toLowerCase(); - } - if (type === 'value') { - return defaultEncoder(str, defaultEncoder, charset, type).toUpperCase(); - } - throw 'this should never happen! type: ' + type; - }; - - // st.deepEqual(stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE'); - expect(stringify({ KeY: 'vAlUe' }, { encoder: encoder })).toBe('key=VALUE'); - }); - - test('objects inside arrays', function () { - var obj = { a: { b: { c: 'd', e: 'f' } } }; - var withArray = { a: { b: [{ c: 'd', e: 'f' }] } }; - - // st.equal( - // stringify(obj, { encode: false }), - // 'a[b][c]=d&a[b][e]=f', - // 'no array, no arrayFormat', - // ); - // st.equal( - // stringify(obj, { encode: false, arrayFormat: 'brackets' }), - // 'a[b][c]=d&a[b][e]=f', - // 'no array, bracket', - // ); - // st.equal( - // stringify(obj, { encode: false, arrayFormat: 'indices' }), - // 'a[b][c]=d&a[b][e]=f', - // 'no array, indices', - // ); - // st.equal( - // stringify(obj, { encode: false, arrayFormat: 'repeat' }), - // 'a[b][c]=d&a[b][e]=f', - // 'no array, repeat', - // ); - // st.equal( - // stringify(obj, { encode: false, arrayFormat: 'comma' }), - // 'a[b][c]=d&a[b][e]=f', - // 'no array, comma', - // ); - expect(stringify(obj, { encode: false })).toBe('a[b][c]=d&a[b][e]=f'); - expect(stringify(obj, { encode: false, arrayFormat: 'brackets' })).toBe('a[b][c]=d&a[b][e]=f'); - expect(stringify(obj, { encode: false, arrayFormat: 'indices' })).toBe('a[b][c]=d&a[b][e]=f'); - expect(stringify(obj, { encode: false, arrayFormat: 'repeat' })).toBe('a[b][c]=d&a[b][e]=f'); - expect(stringify(obj, { encode: false, arrayFormat: 'comma' })).toBe('a[b][c]=d&a[b][e]=f'); - - // st.equal( - // stringify(withArray, { encode: false }), - // 'a[b][0][c]=d&a[b][0][e]=f', - // 'array, no arrayFormat', - // ); - // st.equal( - // stringify(withArray, { encode: false, arrayFormat: 'brackets' }), - // 'a[b][][c]=d&a[b][][e]=f', - // 'array, bracket', - // ); - // st.equal( - // stringify(withArray, { encode: false, arrayFormat: 'indices' }), - // 'a[b][0][c]=d&a[b][0][e]=f', - // 'array, indices', - // ); - // st.equal( - // stringify(withArray, { encode: false, arrayFormat: 'repeat' }), - // 'a[b][c]=d&a[b][e]=f', - // 'array, repeat', - // ); - // st.equal( - // stringify(withArray, { encode: false, arrayFormat: 'comma' }), - // '???', - // 'array, comma', - // { skip: 'TODO: figure out what this should do' }, - // ); - expect(stringify(withArray, { encode: false })).toBe('a[b][0][c]=d&a[b][0][e]=f'); - expect(stringify(withArray, { encode: false, arrayFormat: 'brackets' })).toBe('a[b][][c]=d&a[b][][e]=f'); - expect(stringify(withArray, { encode: false, arrayFormat: 'indices' })).toBe('a[b][0][c]=d&a[b][0][e]=f'); - expect(stringify(withArray, { encode: false, arrayFormat: 'repeat' })).toBe('a[b][c]=d&a[b][e]=f'); - // !TODo: Figure out what this should do - // expect(stringify(withArray, { encode: false, arrayFormat: 'comma' })).toBe( - // 'a[b][c]=d&a[b][e]=f', - // ); - }); - - test('stringifies sparse arrays', function () { - // st.equal( - // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - // 'a[1]=2&a[4]=1', - // ); - // st.equal( - // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - // 'a[]=2&a[]=1', - // ); - // st.equal( - // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), - // 'a=2&a=1', - // ); - expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( - 'a[1]=2&a[4]=1', - ); - expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( - 'a[]=2&a[]=1', - ); - expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' })).toBe( - 'a=2&a=1', - ); - - // st.equal( - // stringify( - // { a: [, { b: [, , { c: '1' }] }] }, - // { encodeValuesOnly: true, arrayFormat: 'indices' }, - // ), - // 'a[1][b][2][c]=1', - // ); - // st.equal( - // stringify( - // { a: [, { b: [, , { c: '1' }] }] }, - // { encodeValuesOnly: true, arrayFormat: 'brackets' }, - // ), - // 'a[][b][][c]=1', - // ); - // st.equal( - // stringify( - // { a: [, { b: [, , { c: '1' }] }] }, - // { encodeValuesOnly: true, arrayFormat: 'repeat' }, - // ), - // 'a[b][c]=1', - // ); - expect( - stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - ).toBe('a[1][b][2][c]=1'); - expect( - stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - ).toBe('a[][b][][c]=1'); - expect( - stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), - ).toBe('a[b][c]=1'); - - // st.equal( - // stringify( - // { a: [, [, , [, , , { c: '1' }]]] }, - // { encodeValuesOnly: true, arrayFormat: 'indices' }, - // ), - // 'a[1][2][3][c]=1', - // ); - // st.equal( - // stringify( - // { a: [, [, , [, , , { c: '1' }]]] }, - // { encodeValuesOnly: true, arrayFormat: 'brackets' }, - // ), - // 'a[][][][c]=1', - // ); - // st.equal( - // stringify( - // { a: [, [, , [, , , { c: '1' }]]] }, - // { encodeValuesOnly: true, arrayFormat: 'repeat' }, - // ), - // 'a[c]=1', - // ); - expect( - stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - ).toBe('a[1][2][3][c]=1'); - expect( - stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - ).toBe('a[][][][c]=1'); - expect( - stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), - ).toBe('a[c]=1'); - - // st.equal( - // stringify( - // { a: [, [, , [, , , { c: [, '1'] }]]] }, - // { encodeValuesOnly: true, arrayFormat: 'indices' }, - // ), - // 'a[1][2][3][c][1]=1', - // ); - // st.equal( - // stringify( - // { a: [, [, , [, , , { c: [, '1'] }]]] }, - // { encodeValuesOnly: true, arrayFormat: 'brackets' }, - // ), - // 'a[][][][c][]=1', - // ); - // st.equal( - // stringify( - // { a: [, [, , [, , , { c: [, '1'] }]]] }, - // { encodeValuesOnly: true, arrayFormat: 'repeat' }, - // ), - // 'a[c]=1', - // ); - expect( - stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), - ).toBe('a[1][2][3][c][1]=1'); - expect( - stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), - ).toBe('a[][][][c][]=1'); - expect( - stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), - ).toBe('a[c]=1'); - }); - - test('encodes a very long string', function () { - var chars = []; - var expected = []; - for (var i = 0; i < 5e3; i++) { - chars.push(' ' + i); - - expected.push('%20' + i); - } - - var obj = { - foo: chars.join(''), - }; - - // st.equal( - // stringify(obj, { arrayFormat: 'bracket', charset: 'utf-8' }), - // 'foo=' + expected.join(''), - // ); - // @ts-expect-error - expect(stringify(obj, { arrayFormat: 'bracket', charset: 'utf-8' })).toBe('foo=' + expected.join('')); - }); -}); - -describe('stringifies empty keys', function () { - empty_test_cases.forEach(function (testCase) { - test('stringifies an object with empty string key with ' + testCase.input, function () { - // st.deepEqual( - // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'indices' }), - // testCase.stringifyOutput.indices, - // 'test case: ' + testCase.input + ', indices', - // ); - // st.deepEqual( - // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'brackets' }), - // testCase.stringifyOutput.brackets, - // 'test case: ' + testCase.input + ', brackets', - // ); - // st.deepEqual( - // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'repeat' }), - // testCase.stringifyOutput.repeat, - // 'test case: ' + testCase.input + ', repeat', - // ); - expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'indices' })).toBe( - testCase.stringify_output.indices, - ); - expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'brackets' })).toBe( - testCase.stringify_output.brackets, - ); - expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'repeat' })).toBe( - testCase.stringify_output.repeat, - ); - }); - }); - - test('edge case with object/arrays', function () { - // st.deepEqual(stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3'); - // st.deepEqual( - // stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), - // '[][0]=2&[][1]=3&[a]=2', - // ); - // st.deepEqual( - // stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' }), - // '[][0]=2&[][1]=3', - // ); - // st.deepEqual( - // stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' }), - // '[][0]=2&[][1]=3&[a]=2', - // ); - expect(stringify({ '': { '': [2, 3] } }, { encode: false })).toBe('[][0]=2&[][1]=3'); - expect(stringify({ '': { '': [2, 3], a: 2 } }, { encode: false })).toBe('[][0]=2&[][1]=3&[a]=2'); - expect(stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' })).toBe( - '[][0]=2&[][1]=3', - ); - expect(stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' })).toBe( - '[][0]=2&[][1]=3&[a]=2', - ); - }); -}); diff --git a/tests/qs/utils.test.ts b/tests/qs/utils.test.ts deleted file mode 100644 index 80b2700..0000000 --- a/tests/qs/utils.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { combine, merge, is_buffer, assign_single_source } from '@beeper/desktop-api/internal/qs/utils'; - -describe('merge()', function () { - // t.deepEqual(merge(null, true), [null, true], 'merges true into null'); - expect(merge(null, true)).toEqual([null, true]); - - // t.deepEqual(merge(null, [42]), [null, 42], 'merges null into an array'); - expect(merge(null, [42])).toEqual([null, 42]); - - // t.deepEqual( - // merge({ a: 'b' }, { a: 'c' }), - // { a: ['b', 'c'] }, - // 'merges two objects with the same key', - // ); - expect(merge({ a: 'b' }, { a: 'c' })).toEqual({ a: ['b', 'c'] }); - - var oneMerged = merge({ foo: 'bar' }, { foo: { first: '123' } }); - // t.deepEqual( - // oneMerged, - // { foo: ['bar', { first: '123' }] }, - // 'merges a standalone and an object into an array', - // ); - expect(oneMerged).toEqual({ foo: ['bar', { first: '123' }] }); - - var twoMerged = merge({ foo: ['bar', { first: '123' }] }, { foo: { second: '456' } }); - // t.deepEqual( - // twoMerged, - // { foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }, - // 'merges a standalone and two objects into an array', - // ); - expect(twoMerged).toEqual({ foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }); - - var sandwiched = merge({ foo: ['bar', { first: '123', second: '456' }] }, { foo: 'baz' }); - // t.deepEqual( - // sandwiched, - // { foo: ['bar', { first: '123', second: '456' }, 'baz'] }, - // 'merges an object sandwiched by two standalones into an array', - // ); - expect(sandwiched).toEqual({ foo: ['bar', { first: '123', second: '456' }, 'baz'] }); - - var nestedArrays = merge({ foo: ['baz'] }, { foo: ['bar', 'xyzzy'] }); - // t.deepEqual(nestedArrays, { foo: ['baz', 'bar', 'xyzzy'] }); - expect(nestedArrays).toEqual({ foo: ['baz', 'bar', 'xyzzy'] }); - - var noOptionsNonObjectSource = merge({ foo: 'baz' }, 'bar'); - // t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true }); - expect(noOptionsNonObjectSource).toEqual({ foo: 'baz', bar: true }); - - (typeof Object.defineProperty !== 'function' ? test.skip : test)( - 'avoids invoking array setters unnecessarily', - function () { - var setCount = 0; - var getCount = 0; - var observed: any[] = []; - Object.defineProperty(observed, 0, { - get: function () { - getCount += 1; - return { bar: 'baz' }; - }, - set: function () { - setCount += 1; - }, - }); - merge(observed, [null]); - // st.equal(setCount, 0); - // st.equal(getCount, 1); - expect(setCount).toEqual(0); - expect(getCount).toEqual(1); - observed[0] = observed[0]; - // st.equal(setCount, 1); - // st.equal(getCount, 2); - expect(setCount).toEqual(1); - expect(getCount).toEqual(2); - }, - ); -}); - -test('assign()', function () { - var target = { a: 1, b: 2 }; - var source = { b: 3, c: 4 }; - var result = assign_single_source(target, source); - - expect(result).toEqual(target); - expect(target).toEqual({ a: 1, b: 3, c: 4 }); - expect(source).toEqual({ b: 3, c: 4 }); -}); - -describe('combine()', function () { - test('both arrays', function () { - var a = [1]; - var b = [2]; - var combined = combine(a, b); - - // st.deepEqual(a, [1], 'a is not mutated'); - // st.deepEqual(b, [2], 'b is not mutated'); - // st.notEqual(a, combined, 'a !== combined'); - // st.notEqual(b, combined, 'b !== combined'); - // st.deepEqual(combined, [1, 2], 'combined is a + b'); - expect(a).toEqual([1]); - expect(b).toEqual([2]); - expect(combined).toEqual([1, 2]); - expect(a).not.toEqual(combined); - expect(b).not.toEqual(combined); - }); - - test('one array, one non-array', function () { - var aN = 1; - var a = [aN]; - var bN = 2; - var b = [bN]; - - var combinedAnB = combine(aN, b); - // st.deepEqual(b, [bN], 'b is not mutated'); - // st.notEqual(aN, combinedAnB, 'aN + b !== aN'); - // st.notEqual(a, combinedAnB, 'aN + b !== a'); - // st.notEqual(bN, combinedAnB, 'aN + b !== bN'); - // st.notEqual(b, combinedAnB, 'aN + b !== b'); - // st.deepEqual([1, 2], combinedAnB, 'first argument is array-wrapped when not an array'); - expect(b).toEqual([bN]); - expect(combinedAnB).not.toEqual(aN); - expect(combinedAnB).not.toEqual(a); - expect(combinedAnB).not.toEqual(bN); - expect(combinedAnB).not.toEqual(b); - expect(combinedAnB).toEqual([1, 2]); - - var combinedABn = combine(a, bN); - // st.deepEqual(a, [aN], 'a is not mutated'); - // st.notEqual(aN, combinedABn, 'a + bN !== aN'); - // st.notEqual(a, combinedABn, 'a + bN !== a'); - // st.notEqual(bN, combinedABn, 'a + bN !== bN'); - // st.notEqual(b, combinedABn, 'a + bN !== b'); - // st.deepEqual([1, 2], combinedABn, 'second argument is array-wrapped when not an array'); - expect(a).toEqual([aN]); - expect(combinedABn).not.toEqual(aN); - expect(combinedABn).not.toEqual(a); - expect(combinedABn).not.toEqual(bN); - expect(combinedABn).not.toEqual(b); - expect(combinedABn).toEqual([1, 2]); - }); - - test('neither is an array', function () { - var combined = combine(1, 2); - // st.notEqual(1, combined, '1 + 2 !== 1'); - // st.notEqual(2, combined, '1 + 2 !== 2'); - // st.deepEqual([1, 2], combined, 'both arguments are array-wrapped when not an array'); - expect(combined).not.toEqual(1); - expect(combined).not.toEqual(2); - expect(combined).toEqual([1, 2]); - }); -}); - -test('is_buffer()', function () { - for (const x of [null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], function () {}, /a/g]) { - // t.equal(is_buffer(x), false, inspect(x) + ' is not a buffer'); - expect(is_buffer(x)).toEqual(false); - } - - var fakeBuffer = { constructor: Buffer }; - // t.equal(is_buffer(fakeBuffer), false, 'fake buffer is not a buffer'); - expect(is_buffer(fakeBuffer)).toEqual(false); - - var saferBuffer = Buffer.from('abc'); - // t.equal(is_buffer(saferBuffer), true, 'SaferBuffer instance is a buffer'); - expect(is_buffer(saferBuffer)).toEqual(true); - - var buffer = Buffer.from('abc'); - // t.equal(is_buffer(buffer), true, 'real Buffer instance is a buffer'); - expect(is_buffer(buffer)).toEqual(true); -}); diff --git a/tests/stringifyQuery.test.ts b/tests/stringifyQuery.test.ts index 1714856..964129f 100644 --- a/tests/stringifyQuery.test.ts +++ b/tests/stringifyQuery.test.ts @@ -20,4 +20,10 @@ describe(stringifyQuery, () => { expect(stringifyQuery(input)).toEqual(expected); }); } + + for (const value of [[], {}, new Date()]) { + it(`${JSON.stringify(value)} -> `, () => { + expect(() => stringifyQuery({ value })).toThrow(`Cannot stringify type ${typeof value}`); + }); + } }); From f50310b4a7cc3e2286430530c8ffc867f77f7ef9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:57:13 +0000 Subject: [PATCH 21/36] chore: configure new SDK language --- .stats.yml | 2 +- packages/mcp-server/README.md | 39 +++++++++++++++++-- packages/mcp-server/build | 2 +- packages/mcp-server/jest.config.ts | 4 +- packages/mcp-server/manifest.json | 2 +- packages/mcp-server/package.json | 6 +-- packages/mcp-server/src/tools/index.ts | 4 ++ .../mcp-server/src/tools/token/info-token.ts | 34 ++++++++++++++++ packages/mcp-server/tsconfig.build.json | 4 +- packages/mcp-server/tsconfig.json | 4 +- 10 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 packages/mcp-server/src/tools/token/info-token.ts diff --git a/.stats.yml b/.stats.yml index 0b6da88..031b180 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 1 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 -config_hash: 1daafc45928b16902d3d0d547fc99d8e +config_hash: 9c6857278ff505b0a79ecd0da31170f9 diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 8c9032d..cf5ec3d 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -17,7 +17,7 @@ You can run the MCP Server directly via `npx`: ```sh export BEEPER_ACCESS_TOKEN="My Access Token" -npx -y @beeper/desktop-mcp@latest +npx -y @beeper/desktop-api-mcp@latest ``` ### Via MCP Client @@ -32,7 +32,7 @@ For clients with a configuration JSON, it might look something like this: "mcpServers": { "beeper_desktop_api_api": { "command": "npx", - "args": ["-y", "@beeper/desktop-mcp", "--client=claude", "--tools=all"], + "args": ["-y", "@beeper/desktop-api-mcp", "--client=claude", "--tools=all"], "env": { "BEEPER_ACCESS_TOKEN": "My Access Token" } @@ -175,9 +175,42 @@ 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-mcp/server"; + +// import a specific tool +import infoToken from "@beeper/desktop-api-mcp/tools/token/info-token"; + +// 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: [infoToken, myCustomEndpoint] }); ``` ## Available Tools The following tools are available in this MCP server. + +### Resource `token`: + +- `info_token` (`read`): Returns information about the authenticated user/token diff --git a/packages/mcp-server/build b/packages/mcp-server/build index b94538a..c8808cc 100644 --- a/packages/mcp-server/build +++ b/packages/mcp-server/build @@ -29,7 +29,7 @@ cp tsconfig.dist-src.json dist/src/tsconfig.json chmod +x dist/index.js -DIST_PATH=./dist PKG_IMPORT_PATH=@beeper/desktop-mcp/ node ../../scripts/utils/postprocess-files.cjs +DIST_PATH=./dist PKG_IMPORT_PATH=@beeper/desktop-api-mcp/ node ../../scripts/utils/postprocess-files.cjs # mcp bundle rm -rf dist-bundle beeper_desktop_api_api.mcpb; mkdir dist-bundle diff --git a/packages/mcp-server/jest.config.ts b/packages/mcp-server/jest.config.ts index 5e54047..f660356 100644 --- a/packages/mcp-server/jest.config.ts +++ b/packages/mcp-server/jest.config.ts @@ -7,8 +7,8 @@ const config: JestConfigWithTsJest = { '^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }], }, moduleNameMapper: { - '^@beeper/desktop-mcp$': '/src/index.ts', - '^@beeper/desktop-mcp/(.*)$': '/src/$1', + '^@beeper/desktop-api-mcp$': '/src/index.ts', + '^@beeper/desktop-api-mcp/(.*)$': '/src/$1', }, modulePathIgnorePatterns: ['/dist/'], testPathIgnorePatterns: ['scripts'], diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 09047c8..212d9d1 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,6 +1,6 @@ { "dxt_version": "0.2", - "name": "@beeper/desktop-mcp", + "name": "@beeper/desktop-api-mcp", "version": "0.1.4", "description": "The official MCP Server for the Beeper Desktop API", "author": { diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index b9510db..9234403 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,5 +1,5 @@ { - "name": "@beeper/desktop-mcp", + "name": "@beeper/desktop-api-mcp", "version": "0.1.5", "description": "The official MCP Server for the Beeper Desktop API", "author": "Beeper Desktop ", @@ -68,8 +68,8 @@ "typescript": "5.8.3" }, "imports": { - "@beeper/desktop-mcp": ".", - "@beeper/desktop-mcp/*": "./src/*" + "@beeper/desktop-api-mcp": ".", + "@beeper/desktop-api-mcp/*": "./src/*" }, "exports": { ".": { diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index 1e1c40a..b976d8d 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -4,12 +4,16 @@ import { Metadata, Endpoint, HandlerFunction } from './types'; export { Metadata, Endpoint, HandlerFunction }; +import info_token from './token/info-token'; + export const endpoints: Endpoint[] = []; function addEndpoint(endpoint: Endpoint) { endpoints.push(endpoint); } +addEndpoint(info_token); + export type Filter = { type: 'resource' | 'operation' | 'tag' | 'tool'; op: 'include' | 'exclude'; diff --git a/packages/mcp-server/src/tools/token/info-token.ts b/packages/mcp-server/src/tools/token/info-token.ts new file mode 100644 index 0000000..2ef864a --- /dev/null +++ b/packages/mcp-server/src/tools/token/info-token.ts @@ -0,0 +1,34 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'token', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/oauth/userinfo', + operationId: 'oauth_get_user_info', +}; + +export const tool: Tool = { + name: 'info_token', + description: 'Returns information about the authenticated user/token', + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + return asTextContentResult(await client.token.info()); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/tsconfig.build.json b/packages/mcp-server/tsconfig.build.json index c1fe977..a346070 100644 --- a/packages/mcp-server/tsconfig.build.json +++ b/packages/mcp-server/tsconfig.build.json @@ -5,8 +5,8 @@ "compilerOptions": { "rootDir": "./dist/src", "paths": { - "@beeper/desktop-mcp/*": ["./dist/src/*"], - "@beeper/desktop-mcp": ["./dist/src/index.ts"] + "@beeper/desktop-api-mcp/*": ["./dist/src/*"], + "@beeper/desktop-api-mcp": ["./dist/src/index.ts"] }, "noEmit": false, "declaration": true, diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index c70b6cc..d7639fe 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -8,8 +8,8 @@ "moduleResolution": "node", "esModuleInterop": true, "paths": { - "@beeper/desktop-mcp/*": ["./src/*"], - "@beeper/desktop-mcp": ["./src/index.ts"] + "@beeper/desktop-api-mcp/*": ["./src/*"], + "@beeper/desktop-api-mcp": ["./src/index.ts"] }, "noEmit": true, From 80ad195afec335513e20cbfcfabb4134cd701bbd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:57:37 +0000 Subject: [PATCH 22/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 031b180..abfa216 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 1 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 -config_hash: 9c6857278ff505b0a79ecd0da31170f9 +config_hash: def03aa92de3408ec65438763617f5c7 From 1434ac78b72e34fbc54e43c26d4a13f72c4617a2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:58:12 +0000 Subject: [PATCH 23/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index abfa216..ab027c2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 1 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 -config_hash: def03aa92de3408ec65438763617f5c7 +config_hash: f83b2b6eb86f2dd68101065998479cb2 From 540c168bfa982178e5ab743c9ef3986784438617 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:58:54 +0000 Subject: [PATCH 24/36] feat(api): manual updates --- .stats.yml | 6 +- README.md | 78 +- api.md | 47 + packages/mcp-server/README.md | 48 +- .../src/tools/accounts/get-accounts.ts | 34 + .../src/tools/chats/archive-chat.ts | 43 + .../src/tools/chats/create-chats.ts | 60 + .../mcp-server/src/tools/chats/get-chat.ts | 46 + .../chats/reminders/clear-chat-reminder.ts | 41 + .../chats/reminders/set-chat-reminder.ts | 53 + .../src/tools/chats/search-chats.ts | 104 + .../src/tools/contacts/search-contacts.ts | 45 + packages/mcp-server/src/tools/index.ts | 26 + .../src/tools/messages/search-messages.ts | 124 + .../src/tools/messages/send-message.ts | 47 + .../tools/top-level/download-asset-client.ts | 38 + .../src/tools/top-level/open-in-app.ts | 52 + .../mcp-server/src/tools/top-level/search.ts | 41 + src/client.ts | 134 +- src/internal/qs/LICENSE.md | 13 + src/internal/qs/README.md | 3 + src/internal/qs/formats.ts | 10 + src/internal/qs/index.ts | 13 + src/internal/qs/stringify.ts | 385 +++ src/internal/qs/types.ts | 71 + src/internal/qs/utils.ts | 265 ++ src/resources/accounts.ts | 31 +- src/resources/chats/chats.ts | 202 +- src/resources/chats/index.ts | 13 +- src/resources/chats/reminders.ts | 67 +- src/resources/contacts.ts | 36 +- src/resources/index.ts | 30 +- src/resources/messages.ts | 140 +- src/resources/shared.ts | 40 +- src/resources/top-level.ts | 125 + tests/api-resources/accounts.test.ts | 21 + tests/api-resources/chats/chats.test.ts | 116 + tests/api-resources/chats/reminders.test.ts | 40 + tests/api-resources/contacts.test.ts | 31 + tests/api-resources/messages.test.ts | 68 + tests/api-resources/top-level.test.ts | 66 + tests/qs/empty-keys-cases.ts | 271 ++ tests/qs/stringify.test.ts | 2232 +++++++++++++++++ tests/qs/utils.test.ts | 169 ++ tests/stringifyQuery.test.ts | 6 - 45 files changed, 5444 insertions(+), 87 deletions(-) create mode 100644 packages/mcp-server/src/tools/accounts/get-accounts.ts create mode 100644 packages/mcp-server/src/tools/chats/archive-chat.ts create mode 100644 packages/mcp-server/src/tools/chats/create-chats.ts create mode 100644 packages/mcp-server/src/tools/chats/get-chat.ts create mode 100644 packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts create mode 100644 packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts create mode 100644 packages/mcp-server/src/tools/chats/search-chats.ts create mode 100644 packages/mcp-server/src/tools/contacts/search-contacts.ts create mode 100644 packages/mcp-server/src/tools/messages/search-messages.ts create mode 100644 packages/mcp-server/src/tools/messages/send-message.ts create mode 100644 packages/mcp-server/src/tools/top-level/download-asset-client.ts create mode 100644 packages/mcp-server/src/tools/top-level/open-in-app.ts create mode 100644 packages/mcp-server/src/tools/top-level/search.ts create mode 100644 src/internal/qs/LICENSE.md create mode 100644 src/internal/qs/README.md create mode 100644 src/internal/qs/formats.ts create mode 100644 src/internal/qs/index.ts create mode 100644 src/internal/qs/stringify.ts create mode 100644 src/internal/qs/types.ts create mode 100644 src/internal/qs/utils.ts create mode 100644 src/resources/top-level.ts create mode 100644 tests/api-resources/accounts.test.ts create mode 100644 tests/api-resources/chats/chats.test.ts create mode 100644 tests/api-resources/chats/reminders.test.ts create mode 100644 tests/api-resources/contacts.test.ts create mode 100644 tests/api-resources/messages.test.ts create mode 100644 tests/api-resources/top-level.test.ts create mode 100644 tests/qs/empty-keys-cases.ts create mode 100644 tests/qs/stringify.test.ts create mode 100644 tests/qs/utils.test.ts diff --git a/.stats.yml b/.stats.yml index ab027c2..8526f3e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 1 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-8c712fe19f280b0b89ecc8a3ce61e9f6b165cee97ce33f66c66a7a5db339c755.yml -openapi_spec_hash: 1ea71129cc1a1ccc3dc8a99566082311 +configured_endpoints: 14 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml +openapi_spec_hash: ba834200758376aaea47b2a276f64c1b config_hash: f83b2b6eb86f2dd68101065998479cb2 diff --git a/README.md b/README.md index 6443d55..5f99160 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ const client = new BeeperDesktop({ accessToken: process.env['BEEPER_ACCESS_TOKEN'], // This is the default and can be omitted }); -const userInfo = await client.token.info(); +const page = await client.chats.search({ includeMuted: true, limit: 3, type: 'single' }); +const chat = page.items[0]; -console.log(userInfo.sub); +console.log(chat.id); ``` ### Request & Response types @@ -41,7 +42,7 @@ const client = new BeeperDesktop({ accessToken: process.env['BEEPER_ACCESS_TOKEN'], // This is the default and can be omitted }); -const userInfo: BeeperDesktop.UserInfo = await client.token.info(); +const accounts: BeeperDesktop.AccountListResponse = 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. @@ -54,15 +55,17 @@ a subclass of `APIError` will be thrown: ```ts -const userInfo = await client.token.info().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; - } -}); +const response = await client.messages + .send({ chatID: '1229391', text: 'Hello! Just checking in on the project status.' }) + .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: @@ -94,7 +97,7 @@ const client = new BeeperDesktop({ }); // Or, configure per-request: -await client.token.info({ +await client.accounts.list({ maxRetries: 5, }); ``` @@ -111,7 +114,7 @@ const client = new BeeperDesktop({ }); // Override per-request: -await client.token.info({ +await client.accounts.list({ timeout: 5 * 1000, }); ``` @@ -120,6 +123,45 @@ 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({ + accountIDs: ['local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI'], + limit: 10, + query: 'deployment', + })) { + allMessages.push(message); + } + return allMessages; +} +``` + +Alternatively, you can request a single page at a time: + +```ts +let page = await client.messages.search({ + accountIDs: ['local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI'], + limit: 10, + query: 'deployment', +}); +for (const message of page.items) { + 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) @@ -134,13 +176,13 @@ Unlike `.asResponse()` this method consumes the body, returning once it is parse ```ts const client = new BeeperDesktop(); -const response = await client.token.info().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: userInfo, response: raw } = await client.token.info().withResponse(); +const { data: accounts, response: raw } = await client.accounts.list().withResponse(); console.log(raw.headers.get('X-My-Header')); -console.log(userInfo.sub); +console.log(accounts); ``` ### Logging @@ -220,7 +262,7 @@ parameter. This library doesn't validate at runtime that the request matches the send will be sent as-is. ```ts -client.token.info({ +client.chats.search({ // ... // @ts-expect-error baz is not yet public baz: 'undocumented option', diff --git a/api.md b/api.md index cc7ad32..ab0991c 100644 --- a/api.md +++ b/api.md @@ -1,5 +1,17 @@ # BeeperDesktop +Types: + +- DownloadAssetResponse +- OpenResponse +- SearchResponse + +Methods: + +- client.downloadAsset({ ...params }) -> DownloadAssetResponse +- client.open({ ...params }) -> OpenResponse +- client.search({ ...params }) -> SearchResponse + # Shared Types: @@ -16,19 +28,54 @@ Types: Types: - Account +- AccountListResponse + +Methods: + +- client.accounts.list() -> AccountListResponse # Contacts +Types: + +- ContactSearchResponse + +Methods: + +- client.contacts.search({ ...params }) -> ContactSearchResponse + # Chats Types: - Chat +- ChatCreateResponse + +Methods: + +- client.chats.create({ ...params }) -> ChatCreateResponse +- client.chats.retrieve(chatID, { ...params }) -> Chat +- client.chats.archive(chatID, { ...params }) -> BaseResponse +- client.chats.search({ ...params }) -> ChatsCursor ## Reminders +Methods: + +- client.chats.reminders.create(chatID, { ...params }) -> BaseResponse +- client.chats.reminders.delete(chatID) -> BaseResponse + # Messages +Types: + +- MessageSendResponse + +Methods: + +- client.messages.search({ ...params }) -> MessagesCursor +- client.messages.send({ ...params }) -> MessageSendResponse + # Token Types: diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index cf5ec3d..ad90c59 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -59,6 +59,7 @@ 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 +- `--tag` includes a set of endpoints with custom tags provided ### Dynamic tools @@ -179,7 +180,7 @@ http://localhost:3000?client=cursor&capability=tool-name-length%3D40 import { server, endpoints, init } from "@beeper/desktop-api-mcp/server"; // import a specific tool -import infoToken from "@beeper/desktop-api-mcp/tools/token/info-token"; +import downloadAssetClient from "@beeper/desktop-api-mcp/tools/top-level/download-asset-client"; // initialize the server and all endpoints init({ server, endpoints }); @@ -204,13 +205,56 @@ const myCustomEndpoint = { }; // initialize the server with your custom endpoints -init({ server: myServer, endpoints: [infoToken, myCustomEndpoint] }); +init({ server: myServer, endpoints: [downloadAssetClient, myCustomEndpoint] }); ``` ## Available Tools The following tools are available in this MCP server. +### Resource `$client`: + +- `download_asset_client` (`write`): Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL. +- `open_in_app` (`write`) tags: [app]: Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment. +- `search` (`read`) tags: [app]: Search for chats, participant name matches in groups, and the first page of messages in one call. Use this when the user asks for a specific chat, group, or person. + +### Resource `accounts`: + +- `get_accounts` (`read`) tags: [accounts]: List connected accounts on this device. Use to pick account context. + +### Resource `contacts`: + +- `search_contacts` (`read`): Search contacts across on a specific account using the network's search API. Only use for creating new chats. + +### Resource `chats`: + +- `create_chats` (`write`): Create a single or group chat on a specific account using participant IDs and optional title. +- `get_chat` (`read`) tags: [chats]: Get chat details: metadata, participants (limited), last activity. +- `archive_chat` (`write`) tags: [chats]: Archive or unarchive a chat. +- `search_chats` (`read`) tags: [chats]: Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'. + +### Resource `chats.reminders`: + +- `set_chat_reminder` (`write`) tags: [chats]: Set a reminder for a chat at a specific time. +- `clear_chat_reminder` (`write`) tags: [chats]: Clear a chat reminder. + +### Resource `messages`: + +- `search_messages` (`read`) tags: [messages]: 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". + - 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 scope="participants" in search-chats) + Returns: matching messages and referenced chats. +- `send_message` (`write`) tags: [messages]: 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 `token`: - `info_token` (`read`): Returns information about the authenticated user/token diff --git a/packages/mcp-server/src/tools/accounts/get-accounts.ts b/packages/mcp-server/src/tools/accounts/get-accounts.ts new file mode 100644 index 0000000..c9de4de --- /dev/null +++ b/packages/mcp-server/src/tools/accounts/get-accounts.ts @@ -0,0 +1,34 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'accounts', + operation: 'read', + tags: ['accounts'], + httpMethod: 'get', + httpPath: '/v1/accounts', + operationId: 'getAccounts', +}; + +export const tool: Tool = { + name: 'get_accounts', + description: 'List connected accounts on this device. Use to pick account context.', + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + return asTextContentResult(await client.accounts.list()); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/archive-chat.ts b/packages/mcp-server/src/tools/chats/archive-chat.ts new file mode 100644 index 0000000..7094bda --- /dev/null +++ b/packages/mcp-server/src/tools/chats/archive-chat.ts @@ -0,0 +1,43 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'write', + tags: ['chats'], + httpMethod: 'post', + httpPath: '/v1/chats/{chatID}/archive', + operationId: 'archiveChat', +}; + +export const tool: Tool = { + name: 'archive_chat', + description: 'Archive or unarchive a chat.', + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: + 'The identifier of the chat to archive or unarchive (accepts both chatID and local chat ID)', + }, + archived: { + type: 'boolean', + description: 'True to archive, false to unarchive', + }, + }, + required: ['chatID'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { chatID, ...body } = args as any; + return asTextContentResult(await client.chats.archive(chatID, body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/create-chats.ts b/packages/mcp-server/src/tools/chats/create-chats.ts new file mode 100644 index 0000000..23ee694 --- /dev/null +++ b/packages/mcp-server/src/tools/chats/create-chats.ts @@ -0,0 +1,60 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v1/chats', + operationId: 'createChat', +}; + +export const tool: Tool = { + name: 'create_chats', + description: + 'Create a single or group chat on a specific account using participant IDs and optional title.', + inputSchema: { + type: 'object', + properties: { + accountID: { + type: 'string', + description: 'Account to create the chat on.', + }, + participantIDs: { + type: 'array', + description: 'User IDs to include in the new chat.', + items: { + type: 'string', + }, + }, + type: { + type: 'string', + description: + "Chat type to create: 'single' requires exactly one participantID; 'group' supports multiple participants and optional title.", + enum: ['single', 'group'], + }, + messageText: { + type: 'string', + description: 'Optional first message content if the platform requires it to create the chat.', + }, + title: { + type: 'string', + description: 'Optional title for group chats; ignored for single chats on most platforms.', + }, + }, + required: ['accountID', 'participantIDs', 'type'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.chats.create(body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/get-chat.ts b/packages/mcp-server/src/tools/chats/get-chat.ts new file mode 100644 index 0000000..02f13bf --- /dev/null +++ b/packages/mcp-server/src/tools/chats/get-chat.ts @@ -0,0 +1,46 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'read', + tags: ['chats'], + httpMethod: 'get', + httpPath: '/v1/chats/{chatID}', + operationId: 'getChat', +}; + +export const tool: Tool = { + name: 'get_chat', + description: 'Get chat details: metadata, participants (limited), last activity.', + 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.', + }, + }, + required: ['chatID'], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { chatID, ...body } = args as any; + return asTextContentResult(await client.chats.retrieve(chatID, body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts new file mode 100644 index 0000000..7e07b5c --- /dev/null +++ b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts @@ -0,0 +1,41 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'chats.reminders', + operation: 'write', + tags: ['chats'], + httpMethod: 'delete', + httpPath: '/v1/chats/{chatID}/reminders', + operationId: 'clearChatReminder', +}; + +export const tool: Tool = { + name: 'clear_chat_reminder', + description: 'Clear a chat reminder.', + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: + 'The identifier of the chat to clear reminder from (accepts both chatID and local chat ID)', + }, + }, + required: ['chatID'], + }, + annotations: { + idempotentHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { chatID, ...body } = args as any; + return asTextContentResult(await client.chats.reminders.delete(chatID)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts new file mode 100644 index 0000000..4c3473a --- /dev/null +++ b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts @@ -0,0 +1,53 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'chats.reminders', + operation: 'write', + tags: ['chats'], + httpMethod: 'post', + httpPath: '/v1/chats/{chatID}/reminders', + operationId: 'setChatReminder', +}; + +export const tool: Tool = { + name: 'set_chat_reminder', + description: 'Set a reminder for a chat at a specific time.', + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'The identifier of the chat to set reminder for (accepts both chatID and local chat ID)', + }, + 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'], + }, + }, + required: ['chatID', 'reminder'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const { chatID, ...body } = args as any; + return asTextContentResult(await client.chats.reminders.create(chatID, body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/search-chats.ts b/packages/mcp-server/src/tools/chats/search-chats.ts new file mode 100644 index 0000000..37dabd9 --- /dev/null +++ b/packages/mcp-server/src/tools/chats/search-chats.ts @@ -0,0 +1,104 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'read', + tags: ['chats'], + httpMethod: 'get', + httpPath: '/v1/chats/search', + operationId: 'searchChats', +}; + +export const tool: Tool = { + name: 'search_chats', + description: + "Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'.", + 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', + description: 'Account ID this resource belongs to.', + }, + }, + cursor: { + type: 'string', + description: 'Pagination cursor from previous response. Use with direction to navigate results', + }, + direction: { + type: 'string', + description: + 'Pagination direction: "after" for newer page, "before" for older page. Defaults to "before" when only cursor is provided.', + enum: ['after', 'before'], + }, + 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', + }, + query: { + type: 'string', + description: + 'Literal token search (non-semantic). Use single words users type (e.g., "dinner"). When multiple words provided, ALL must match. Case-insensitive.', + }, + scope: { + type: 'string', + description: + "Search scope: 'titles' matches title + network; 'participants' matches participant names.", + enum: ['titles', 'participants'], + }, + type: { + type: 'string', + description: + 'Specify the type of chats to retrieve: use "single" for direct messages, "group" for group chats, or "any" to get all types', + enum: ['single', 'group', 'any'], + }, + unreadOnly: { + type: 'boolean', + description: 'Set to true to only retrieve chats that have unread messages', + }, + }, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + const response = await client.chats.search(body).asResponse(); + return asTextContentResult(await response.json()); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/contacts/search-contacts.ts b/packages/mcp-server/src/tools/contacts/search-contacts.ts new file mode 100644 index 0000000..b889569 --- /dev/null +++ b/packages/mcp-server/src/tools/contacts/search-contacts.ts @@ -0,0 +1,45 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'contacts', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/v1/contacts/search', + operationId: 'searchContacts', +}; + +export const tool: Tool = { + name: 'search_contacts', + description: + "Search contacts across on a specific account using the network's search API. Only use for creating new chats.", + inputSchema: { + type: 'object', + properties: { + accountID: { + type: 'string', + description: 'Account ID this resource belongs to.', + }, + query: { + type: 'string', + description: 'Text to search users by. Network-specific behavior.', + }, + }, + required: ['accountID', 'query'], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.contacts.search(body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index b976d8d..d35d250 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -4,6 +4,19 @@ import { Metadata, Endpoint, HandlerFunction } from './types'; export { Metadata, Endpoint, HandlerFunction }; +import download_asset_client from './top-level/download-asset-client'; +import open_in_app from './top-level/open-in-app'; +import search from './top-level/search'; +import get_accounts from './accounts/get-accounts'; +import search_contacts from './contacts/search-contacts'; +import create_chats from './chats/create-chats'; +import get_chat from './chats/get-chat'; +import archive_chat from './chats/archive-chat'; +import search_chats from './chats/search-chats'; +import set_chat_reminder from './chats/reminders/set-chat-reminder'; +import clear_chat_reminder from './chats/reminders/clear-chat-reminder'; +import search_messages from './messages/search-messages'; +import send_message from './messages/send-message'; import info_token from './token/info-token'; export const endpoints: Endpoint[] = []; @@ -12,6 +25,19 @@ function addEndpoint(endpoint: Endpoint) { endpoints.push(endpoint); } +addEndpoint(download_asset_client); +addEndpoint(open_in_app); +addEndpoint(search); +addEndpoint(get_accounts); +addEndpoint(search_contacts); +addEndpoint(create_chats); +addEndpoint(get_chat); +addEndpoint(archive_chat); +addEndpoint(search_chats); +addEndpoint(set_chat_reminder); +addEndpoint(clear_chat_reminder); +addEndpoint(search_messages); +addEndpoint(send_message); addEndpoint(info_token); export type Filter = { 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..bbf1ea9 --- /dev/null +++ b/packages/mcp-server/src/tools/messages/search-messages.ts @@ -0,0 +1,124 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'messages', + operation: 'read', + tags: ['messages'], + httpMethod: 'get', + httpPath: '/v1/messages/search', + operationId: 'searchMessages', +}; + +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- 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 scope="participants" in search-chats)\nReturns: matching messages and referenced chats.', + inputSchema: { + type: 'object', + properties: { + accountIDs: { + type: 'array', + description: 'Limit search to specific account IDs.', + items: { + type: 'string', + description: 'Account ID this resource belongs to.', + }, + }, + chatIDs: { + type: 'array', + description: 'Limit search to specific 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'], + }, + cursor: { + type: 'string', + description: "Opaque pagination cursor; do not inspect. Use together with 'direction'.", + }, + 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', + }, + direction: { + type: 'string', + description: + "Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided.", + enum: ['after', 'before'], + }, + 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 20. The current implementation caps each page at 20 items even if a higher limit is requested.', + }, + mediaTypes: { + type: 'array', + description: + "Filter messages by media types. Use ['any'] for any media type, or specify exact types like ['video', 'image']. Omit for no media filtering.", + items: { + type: 'string', + enum: ['any', 'video', 'image', 'link', 'file'], + }, + }, + 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).", + }, + }, + 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-message.ts b/packages/mcp-server/src/tools/messages/send-message.ts new file mode 100644 index 0000000..81d7ba4 --- /dev/null +++ b/packages/mcp-server/src/tools/messages/send-message.ts @@ -0,0 +1,47 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'messages', + operation: 'write', + tags: ['messages'], + httpMethod: 'post', + httpPath: '/v1/messages', + operationId: 'sendMessage', +}; + +export const tool: Tool = { + name: 'send_message', + description: + 'Send a text message to a specific chat. Supports replying to existing messages. Returns the sent message ID and a deeplink to the chat', + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'Unique identifier of the chat.', + }, + 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.', + }, + }, + required: ['chatID'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.messages.send(body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/download-asset-client.ts b/packages/mcp-server/src/tools/top-level/download-asset-client.ts new file mode 100644 index 0000000..ed39eb3 --- /dev/null +++ b/packages/mcp-server/src/tools/top-level/download-asset-client.ts @@ -0,0 +1,38 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: '$client', + operation: 'write', + tags: [], + httpMethod: 'post', + httpPath: '/v1/app/download-asset', + operationId: 'downloadAsset', +}; + +export const tool: Tool = { + name: 'download_asset_client', + description: 'Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL.', + inputSchema: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'Matrix content URL (mxc:// or localmxc://) for the asset to download.', + }, + }, + required: ['url'], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.downloadAsset(body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/open-in-app.ts b/packages/mcp-server/src/tools/top-level/open-in-app.ts new file mode 100644 index 0000000..3743bfe --- /dev/null +++ b/packages/mcp-server/src/tools/top-level/open-in-app.ts @@ -0,0 +1,52 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: '$client', + operation: 'write', + tags: ['app'], + httpMethod: 'post', + httpPath: '/v1/app/open', + operationId: 'openApp', +}; + +export const tool: Tool = { + name: 'open_in_app', + description: + 'Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment.', + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: + 'Optional Beeper chat ID (or local chat ID) to focus after opening the app. If omitted, only opens/focuses the app.', + }, + draftAttachmentPath: { + type: 'string', + description: 'Optional draft attachment path to populate in the message input field.', + }, + draftText: { + type: 'string', + description: 'Optional draft text to populate in the message input field.', + }, + messageID: { + type: 'string', + description: 'Optional message ID. Jumps to that message in the chat when opening.', + }, + }, + required: [], + }, + annotations: {}, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.open(body)); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/search.ts b/packages/mcp-server/src/tools/top-level/search.ts new file mode 100644 index 0000000..aaa483c --- /dev/null +++ b/packages/mcp-server/src/tools/top-level/search.ts @@ -0,0 +1,41 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: '$client', + operation: 'read', + tags: ['app'], + httpMethod: 'get', + httpPath: '/v1/search', + operationId: 'search', +}; + +export const tool: Tool = { + name: 'search', + description: + 'Search for chats, participant name matches in groups, and the first page of messages in one call. Use this when the user asks for a specific chat, group, or person.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'User-typed search text. Literal word matching (NOT semantic).', + }, + }, + required: ['query'], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + return asTextContentResult(await client.search(body)); +}; + +export default { metadata, tool, handler }; diff --git a/src/client.ts b/src/client.ts index 3802ddb..8e93db2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,18 +11,37 @@ import type { APIResponseProps } from './internal/parse'; import { getPlatformHeaders } from './internal/detect-platform'; 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 Pagination from './core/pagination'; import { AbstractPage, type CursorParams, CursorResponse } from './core/pagination'; import * as Uploads from './core/uploads'; import * as API from './resources/index'; +import * as TopLevelAPI from './resources/top-level'; +import { + DownloadAssetParams, + DownloadAssetResponse, + OpenParams, + OpenResponse, + SearchParams, + SearchResponse, +} from './resources/top-level'; import { APIPromise } from './core/api-promise'; -import { Account, Accounts } from './resources/accounts'; -import { Contacts } from './resources/contacts'; -import { Messages } from './resources/messages'; +import { Account, AccountListResponse, Accounts } from './resources/accounts'; +import { ContactSearchParams, ContactSearchResponse, Contacts } from './resources/contacts'; +import { MessageSearchParams, MessageSendParams, MessageSendResponse, Messages } from './resources/messages'; import { Token, UserInfo } from './resources/token'; -import { Chat, Chats } from './resources/chats/chats'; +import { + Chat, + ChatArchiveParams, + ChatCreateParams, + ChatCreateResponse, + ChatRetrieveParams, + ChatSearchParams, + Chats, + ChatsCursor, +} from './resources/chats/chats'; import { type Fetch } from './internal/builtin-types'; import { isRunningInBrowser } from './internal/detect-platform'; import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers'; @@ -218,6 +237,54 @@ export class BeeperDesktop { return this.baseURL !== 'http://localhost:23373'; } + /** + * Download a Matrix asset using its mxc:// or localmxc:// URL and return the local + * file URL. + * + * @example + * ```ts + * const response = await client.downloadAsset({ + * url: 'mxc://example.org/Q4x9CqGz1pB3Oa6XgJ', + * }); + * ``` + */ + downloadAsset( + body: TopLevelAPI.DownloadAssetParams, + options?: RequestOptions, + ): APIPromise { + return this.post('/v1/app/download-asset', { body, ...options }); + } + + /** + * Open Beeper Desktop and optionally navigate to a specific chat, message, or + * pre-fill draft text and attachment. + * + * @example + * ```ts + * const response = await client.open(); + * ``` + */ + open( + body: TopLevelAPI.OpenParams | null | undefined = {}, + options?: RequestOptions, + ): APIPromise { + return this.post('/v1/app/open', { body, ...options }); + } + + /** + * Returns matching chats, participant name matches in groups, and the first page + * of messages in one call. Paginate messages via search-messages. Paginate chats + * via search-chats. Uses the same sorting as the chat search in the app. + * + * @example + * ```ts + * const response = await client.search({ query: 'x' }); + * ``` + */ + search(query: TopLevelAPI.SearchParams, options?: RequestOptions): APIPromise { + return this.get('/v1/search', { query, ...options }); + } + protected defaultQuery(): Record | undefined { return this._options.defaultQuery; } @@ -230,24 +297,8 @@ export class BeeperDesktop { return buildHeaders([{ Authorization: `Bearer ${this.accessToken}` }]); } - /** - * Basic re-implementation of `qs.stringify` for primitive types. - */ protected stringifyQuery(query: Record): string { - return Object.entries(query) - .filter(([_, value]) => typeof value !== 'undefined') - .map(([key, value]) => { - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - } - if (value === null) { - return `${encodeURIComponent(key)}=`; - } - throw new Errors.BeeperDesktopError( - `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, - ); - }) - .join('&'); + return qs.stringify(query, { arrayFormat: 'repeat' }); } private getUserAgent(): string { @@ -787,13 +838,40 @@ export declare namespace BeeperDesktop { export import Cursor = Pagination.Cursor; export { type CursorParams as CursorParams, type CursorResponse as CursorResponse }; - export { Accounts as Accounts, type Account as Account }; - - export { Contacts as Contacts }; - - export { Chats as Chats, type Chat as Chat }; - - export { Messages as Messages }; + export { + type DownloadAssetResponse as DownloadAssetResponse, + type OpenResponse as OpenResponse, + type SearchResponse as SearchResponse, + type DownloadAssetParams as DownloadAssetParams, + type OpenParams as OpenParams, + type SearchParams as SearchParams, + }; + + export { Accounts as Accounts, type Account as Account, type AccountListResponse as AccountListResponse }; + + export { + Contacts as Contacts, + type ContactSearchResponse as ContactSearchResponse, + type ContactSearchParams as ContactSearchParams, + }; + + export { + Chats as Chats, + type Chat as Chat, + type ChatCreateResponse as ChatCreateResponse, + type ChatsCursor as ChatsCursor, + type ChatCreateParams as ChatCreateParams, + type ChatRetrieveParams as ChatRetrieveParams, + type ChatArchiveParams as ChatArchiveParams, + type ChatSearchParams as ChatSearchParams, + }; + + export { + Messages as Messages, + type MessageSendResponse as MessageSendResponse, + type MessageSearchParams as MessageSearchParams, + type MessageSendParams as MessageSendParams, + }; export { Token as Token, type UserInfo as UserInfo }; diff --git a/src/internal/qs/LICENSE.md b/src/internal/qs/LICENSE.md new file mode 100644 index 0000000..3fda157 --- /dev/null +++ b/src/internal/qs/LICENSE.md @@ -0,0 +1,13 @@ +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/puruvj/neoqs/graphs/contributors) All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/internal/qs/README.md b/src/internal/qs/README.md new file mode 100644 index 0000000..67ae04e --- /dev/null +++ b/src/internal/qs/README.md @@ -0,0 +1,3 @@ +# qs + +This is a vendored version of [neoqs](https://github.com/PuruVJ/neoqs) which is a TypeScript rewrite of [qs](https://github.com/ljharb/qs), a query string library. diff --git a/src/internal/qs/formats.ts b/src/internal/qs/formats.ts new file mode 100644 index 0000000..e76a742 --- /dev/null +++ b/src/internal/qs/formats.ts @@ -0,0 +1,10 @@ +import type { Format } from './types'; + +export const default_format: Format = 'RFC3986'; +export const default_formatter = (v: PropertyKey) => String(v); +export const formatters: Record string> = { + RFC1738: (v: PropertyKey) => String(v).replace(/%20/g, '+'), + RFC3986: default_formatter, +}; +export const RFC1738 = 'RFC1738'; +export const RFC3986 = 'RFC3986'; diff --git a/src/internal/qs/index.ts b/src/internal/qs/index.ts new file mode 100644 index 0000000..c3a3620 --- /dev/null +++ b/src/internal/qs/index.ts @@ -0,0 +1,13 @@ +import { default_format, formatters, RFC1738, RFC3986 } from './formats'; + +const formats = { + formatters, + RFC1738, + RFC3986, + default: default_format, +}; + +export { stringify } from './stringify'; +export { formats }; + +export type { DefaultDecoder, DefaultEncoder, Format, ParseOptions, StringifyOptions } from './types'; diff --git a/src/internal/qs/stringify.ts b/src/internal/qs/stringify.ts new file mode 100644 index 0000000..7e71387 --- /dev/null +++ b/src/internal/qs/stringify.ts @@ -0,0 +1,385 @@ +import { encode, is_buffer, maybe_map, has } from './utils'; +import { default_format, default_formatter, formatters } from './formats'; +import type { NonNullableProperties, StringifyOptions } from './types'; +import { isArray } from '../utils/values'; + +const array_prefix_generators = { + brackets(prefix: PropertyKey) { + return String(prefix) + '[]'; + }, + comma: 'comma', + indices(prefix: PropertyKey, key: string) { + return String(prefix) + '[' + key + ']'; + }, + repeat(prefix: PropertyKey) { + return String(prefix); + }, +}; + +const push_to_array = function (arr: any[], value_or_array: any) { + Array.prototype.push.apply(arr, isArray(value_or_array) ? value_or_array : [value_or_array]); +}; + +let toISOString; + +const defaults = { + addQueryPrefix: false, + allowDots: false, + allowEmptyArrays: false, + arrayFormat: 'indices', + charset: 'utf-8', + charsetSentinel: false, + delimiter: '&', + encode: true, + encodeDotInKeys: false, + encoder: encode, + encodeValuesOnly: false, + format: default_format, + formatter: default_formatter, + /** @deprecated */ + indices: false, + serializeDate(date) { + return (toISOString ??= Function.prototype.call.bind(Date.prototype.toISOString))(date); + }, + skipNulls: false, + strictNullHandling: false, +} as NonNullableProperties; + +function is_non_nullish_primitive(v: unknown): v is string | number | boolean | symbol | bigint { + return ( + typeof v === 'string' || + typeof v === 'number' || + typeof v === 'boolean' || + typeof v === 'symbol' || + typeof v === 'bigint' + ); +} + +const sentinel = {}; + +function inner_stringify( + object: any, + prefix: PropertyKey, + generateArrayPrefix: StringifyOptions['arrayFormat'] | ((prefix: string, key: string) => string), + commaRoundTrip: boolean, + allowEmptyArrays: boolean, + strictNullHandling: boolean, + skipNulls: boolean, + encodeDotInKeys: boolean, + encoder: StringifyOptions['encoder'], + filter: StringifyOptions['filter'], + sort: StringifyOptions['sort'], + allowDots: StringifyOptions['allowDots'], + serializeDate: StringifyOptions['serializeDate'], + format: StringifyOptions['format'], + formatter: StringifyOptions['formatter'], + encodeValuesOnly: boolean, + charset: StringifyOptions['charset'], + sideChannel: WeakMap, +) { + let obj = object; + + let tmp_sc = sideChannel; + let step = 0; + let find_flag = false; + while ((tmp_sc = tmp_sc.get(sentinel)) !== void undefined && !find_flag) { + // Where object last appeared in the ref tree + const pos = tmp_sc.get(object); + step += 1; + if (typeof pos !== 'undefined') { + if (pos === step) { + throw new RangeError('Cyclic object value'); + } else { + find_flag = true; // Break while + } + } + if (typeof tmp_sc.get(sentinel) === 'undefined') { + step = 0; + } + } + + if (typeof filter === 'function') { + obj = filter(prefix, obj); + } else if (obj instanceof Date) { + obj = serializeDate?.(obj); + } else if (generateArrayPrefix === 'comma' && isArray(obj)) { + obj = maybe_map(obj, function (value) { + if (value instanceof Date) { + return serializeDate?.(value); + } + return value; + }); + } + + if (obj === null) { + if (strictNullHandling) { + return encoder && !encodeValuesOnly ? + // @ts-expect-error + encoder(prefix, defaults.encoder, charset, 'key', format) + : prefix; + } + + obj = ''; + } + + if (is_non_nullish_primitive(obj) || is_buffer(obj)) { + if (encoder) { + const key_value = + encodeValuesOnly ? prefix + // @ts-expect-error + : encoder(prefix, defaults.encoder, charset, 'key', format); + return [ + formatter?.(key_value) + + '=' + + // @ts-expect-error + formatter?.(encoder(obj, defaults.encoder, charset, 'value', format)), + ]; + } + return [formatter?.(prefix) + '=' + formatter?.(String(obj))]; + } + + const values: string[] = []; + + if (typeof obj === 'undefined') { + return values; + } + + let obj_keys; + if (generateArrayPrefix === 'comma' && isArray(obj)) { + // we need to join elements in + if (encodeValuesOnly && encoder) { + // @ts-expect-error values only + obj = maybe_map(obj, encoder); + } + obj_keys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }]; + } else if (isArray(filter)) { + obj_keys = filter; + } else { + const keys = Object.keys(obj); + obj_keys = sort ? keys.sort(sort) : keys; + } + + const encoded_prefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix); + + const adjusted_prefix = + commaRoundTrip && isArray(obj) && obj.length === 1 ? encoded_prefix + '[]' : encoded_prefix; + + if (allowEmptyArrays && isArray(obj) && obj.length === 0) { + return adjusted_prefix + '[]'; + } + + for (let j = 0; j < obj_keys.length; ++j) { + const key = obj_keys[j]; + const value = + // @ts-ignore + typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key as any]; + + if (skipNulls && value === null) { + continue; + } + + // @ts-ignore + const encoded_key = allowDots && encodeDotInKeys ? (key as any).replace(/\./g, '%2E') : key; + const key_prefix = + isArray(obj) ? + typeof generateArrayPrefix === 'function' ? + generateArrayPrefix(adjusted_prefix, encoded_key) + : adjusted_prefix + : adjusted_prefix + (allowDots ? '.' + encoded_key : '[' + encoded_key + ']'); + + sideChannel.set(object, step); + const valueSideChannel = new WeakMap(); + valueSideChannel.set(sentinel, sideChannel); + push_to_array( + values, + inner_stringify( + value, + key_prefix, + generateArrayPrefix, + commaRoundTrip, + allowEmptyArrays, + strictNullHandling, + skipNulls, + encodeDotInKeys, + // @ts-ignore + generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder, + filter, + sort, + allowDots, + serializeDate, + format, + formatter, + encodeValuesOnly, + charset, + valueSideChannel, + ), + ); + } + + return values; +} + +function normalize_stringify_options( + opts: StringifyOptions = defaults, +): NonNullableProperties> & { indices?: boolean } { + if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') { + throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided'); + } + + if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') { + throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided'); + } + + if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') { + throw new TypeError('Encoder has to be a function.'); + } + + const charset = opts.charset || defaults.charset; + if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { + throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'); + } + + let format = default_format; + if (typeof opts.format !== 'undefined') { + if (!has(formatters, opts.format)) { + throw new TypeError('Unknown format option provided.'); + } + format = opts.format; + } + const formatter = formatters[format]; + + let filter = defaults.filter; + if (typeof opts.filter === 'function' || isArray(opts.filter)) { + filter = opts.filter; + } + + let arrayFormat: StringifyOptions['arrayFormat']; + if (opts.arrayFormat && opts.arrayFormat in array_prefix_generators) { + arrayFormat = opts.arrayFormat; + } else if ('indices' in opts) { + arrayFormat = opts.indices ? 'indices' : 'repeat'; + } else { + arrayFormat = defaults.arrayFormat; + } + + if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') { + throw new TypeError('`commaRoundTrip` must be a boolean, or absent'); + } + + const allowDots = + typeof opts.allowDots === 'undefined' ? + !!opts.encodeDotInKeys === true ? + true + : defaults.allowDots + : !!opts.allowDots; + + return { + addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix, + // @ts-ignore + allowDots: allowDots, + allowEmptyArrays: + typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays, + arrayFormat: arrayFormat, + charset: charset, + charsetSentinel: + typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, + commaRoundTrip: !!opts.commaRoundTrip, + delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter, + encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode, + encodeDotInKeys: + typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys, + encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder, + encodeValuesOnly: + typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly, + filter: filter, + format: format, + formatter: formatter, + serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate, + skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls, + // @ts-ignore + sort: typeof opts.sort === 'function' ? opts.sort : null, + strictNullHandling: + typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling, + }; +} + +export function stringify(object: any, opts: StringifyOptions = {}) { + let obj = object; + const options = normalize_stringify_options(opts); + + let obj_keys: PropertyKey[] | undefined; + let filter; + + if (typeof options.filter === 'function') { + filter = options.filter; + obj = filter('', obj); + } else if (isArray(options.filter)) { + filter = options.filter; + obj_keys = filter; + } + + const keys: string[] = []; + + if (typeof obj !== 'object' || obj === null) { + return ''; + } + + const generateArrayPrefix = array_prefix_generators[options.arrayFormat]; + const commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip; + + if (!obj_keys) { + obj_keys = Object.keys(obj); + } + + if (options.sort) { + obj_keys.sort(options.sort); + } + + const sideChannel = new WeakMap(); + for (let i = 0; i < obj_keys.length; ++i) { + const key = obj_keys[i]!; + + if (options.skipNulls && obj[key] === null) { + continue; + } + push_to_array( + keys, + inner_stringify( + obj[key], + key, + // @ts-expect-error + generateArrayPrefix, + commaRoundTrip, + options.allowEmptyArrays, + options.strictNullHandling, + options.skipNulls, + options.encodeDotInKeys, + options.encode ? options.encoder : null, + options.filter, + options.sort, + options.allowDots, + options.serializeDate, + options.format, + options.formatter, + options.encodeValuesOnly, + options.charset, + sideChannel, + ), + ); + } + + const joined = keys.join(options.delimiter); + let prefix = options.addQueryPrefix === true ? '?' : ''; + + if (options.charsetSentinel) { + if (options.charset === 'iso-8859-1') { + // encodeURIComponent('✓'), the "numeric entity" representation of a checkmark + prefix += 'utf8=%26%2310003%3B&'; + } else { + // encodeURIComponent('✓') + prefix += 'utf8=%E2%9C%93&'; + } + } + + return joined.length > 0 ? prefix + joined : ''; +} diff --git a/src/internal/qs/types.ts b/src/internal/qs/types.ts new file mode 100644 index 0000000..7c28dbb --- /dev/null +++ b/src/internal/qs/types.ts @@ -0,0 +1,71 @@ +export type Format = 'RFC1738' | 'RFC3986'; + +export type DefaultEncoder = (str: any, defaultEncoder?: any, charset?: string) => string; +export type DefaultDecoder = (str: string, decoder?: any, charset?: string) => string; + +export type BooleanOptional = boolean | undefined; + +export type StringifyBaseOptions = { + delimiter?: string; + allowDots?: boolean; + encodeDotInKeys?: boolean; + strictNullHandling?: boolean; + skipNulls?: boolean; + encode?: boolean; + encoder?: ( + str: any, + defaultEncoder: DefaultEncoder, + charset: string, + type: 'key' | 'value', + format?: Format, + ) => string; + filter?: Array | ((prefix: PropertyKey, value: any) => any); + arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma'; + indices?: boolean; + sort?: ((a: PropertyKey, b: PropertyKey) => number) | null; + serializeDate?: (d: Date) => string; + format?: 'RFC1738' | 'RFC3986'; + formatter?: (str: PropertyKey) => string; + encodeValuesOnly?: boolean; + addQueryPrefix?: boolean; + charset?: 'utf-8' | 'iso-8859-1'; + charsetSentinel?: boolean; + allowEmptyArrays?: boolean; + commaRoundTrip?: boolean; +}; + +export type StringifyOptions = StringifyBaseOptions; + +export type ParseBaseOptions = { + comma?: boolean; + delimiter?: string | RegExp; + depth?: number | false; + decoder?: (str: string, defaultDecoder: DefaultDecoder, charset: string, type: 'key' | 'value') => any; + arrayLimit?: number; + parseArrays?: boolean; + plainObjects?: boolean; + allowPrototypes?: boolean; + allowSparse?: boolean; + parameterLimit?: number; + strictDepth?: boolean; + strictNullHandling?: boolean; + ignoreQueryPrefix?: boolean; + charset?: 'utf-8' | 'iso-8859-1'; + charsetSentinel?: boolean; + interpretNumericEntities?: boolean; + allowEmptyArrays?: boolean; + duplicates?: 'combine' | 'first' | 'last'; + allowDots?: boolean; + decodeDotInKeys?: boolean; +}; + +export type ParseOptions = ParseBaseOptions; + +export type ParsedQs = { + [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; +}; + +// Type to remove null or undefined union from each property +export type NonNullableProperties = { + [K in keyof T]-?: Exclude; +}; diff --git a/src/internal/qs/utils.ts b/src/internal/qs/utils.ts new file mode 100644 index 0000000..4cd5657 --- /dev/null +++ b/src/internal/qs/utils.ts @@ -0,0 +1,265 @@ +import { RFC1738 } from './formats'; +import type { DefaultEncoder, Format } from './types'; +import { isArray } from '../utils/values'; + +export let has = (obj: object, key: PropertyKey): boolean => ( + (has = (Object as any).hasOwn ?? Function.prototype.call.bind(Object.prototype.hasOwnProperty)), + has(obj, key) +); + +const hex_table = /* @__PURE__ */ (() => { + const array = []; + for (let i = 0; i < 256; ++i) { + array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase()); + } + + return array; +})(); + +function compact_queue>(queue: Array<{ obj: T; prop: string }>) { + while (queue.length > 1) { + const item = queue.pop(); + if (!item) continue; + + const obj = item.obj[item.prop]; + + if (isArray(obj)) { + const compacted: unknown[] = []; + + for (let j = 0; j < obj.length; ++j) { + if (typeof obj[j] !== 'undefined') { + compacted.push(obj[j]); + } + } + + // @ts-ignore + item.obj[item.prop] = compacted; + } + } +} + +function array_to_object(source: any[], options: { plainObjects: boolean }) { + const obj = options && options.plainObjects ? Object.create(null) : {}; + for (let i = 0; i < source.length; ++i) { + if (typeof source[i] !== 'undefined') { + obj[i] = source[i]; + } + } + + return obj; +} + +export function merge( + target: any, + source: any, + options: { plainObjects?: boolean; allowPrototypes?: boolean } = {}, +) { + if (!source) { + return target; + } + + if (typeof source !== 'object') { + if (isArray(target)) { + target.push(source); + } else if (target && typeof target === 'object') { + if ((options && (options.plainObjects || options.allowPrototypes)) || !has(Object.prototype, source)) { + target[source] = true; + } + } else { + return [target, source]; + } + + return target; + } + + if (!target || typeof target !== 'object') { + return [target].concat(source); + } + + let mergeTarget = target; + if (isArray(target) && !isArray(source)) { + // @ts-ignore + mergeTarget = array_to_object(target, options); + } + + if (isArray(target) && isArray(source)) { + source.forEach(function (item, i) { + if (has(target, i)) { + const targetItem = target[i]; + if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { + target[i] = merge(targetItem, item, options); + } else { + target.push(item); + } + } else { + target[i] = item; + } + }); + return target; + } + + return Object.keys(source).reduce(function (acc, key) { + const value = source[key]; + + if (has(acc, key)) { + acc[key] = merge(acc[key], value, options); + } else { + acc[key] = value; + } + return acc; + }, mergeTarget); +} + +export function assign_single_source(target: any, source: any) { + return Object.keys(source).reduce(function (acc, key) { + acc[key] = source[key]; + return acc; + }, target); +} + +export function decode(str: string, _: any, charset: string) { + const strWithoutPlus = str.replace(/\+/g, ' '); + if (charset === 'iso-8859-1') { + // unescape never throws, no try...catch needed: + return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape); + } + // utf-8 + try { + return decodeURIComponent(strWithoutPlus); + } catch (e) { + return strWithoutPlus; + } +} + +const limit = 1024; + +export const encode: ( + str: any, + defaultEncoder: DefaultEncoder, + charset: string, + type: 'key' | 'value', + format: Format, +) => string = (str, _defaultEncoder, charset, _kind, format: Format) => { + // This code was originally written by Brian White for the io.js core querystring library. + // It has been adapted here for stricter adherence to RFC 3986 + if (str.length === 0) { + return str; + } + + let string = str; + if (typeof str === 'symbol') { + string = Symbol.prototype.toString.call(str); + } else if (typeof str !== 'string') { + string = String(str); + } + + if (charset === 'iso-8859-1') { + return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { + return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; + }); + } + + let out = ''; + for (let j = 0; j < string.length; j += limit) { + const segment = string.length >= limit ? string.slice(j, j + limit) : string; + const arr = []; + + for (let i = 0; i < segment.length; ++i) { + let c = segment.charCodeAt(i); + if ( + c === 0x2d || // - + c === 0x2e || // . + c === 0x5f || // _ + c === 0x7e || // ~ + (c >= 0x30 && c <= 0x39) || // 0-9 + (c >= 0x41 && c <= 0x5a) || // a-z + (c >= 0x61 && c <= 0x7a) || // A-Z + (format === RFC1738 && (c === 0x28 || c === 0x29)) // ( ) + ) { + arr[arr.length] = segment.charAt(i); + continue; + } + + if (c < 0x80) { + arr[arr.length] = hex_table[c]; + continue; + } + + if (c < 0x800) { + arr[arr.length] = hex_table[0xc0 | (c >> 6)]! + hex_table[0x80 | (c & 0x3f)]; + continue; + } + + if (c < 0xd800 || c >= 0xe000) { + arr[arr.length] = + hex_table[0xe0 | (c >> 12)]! + hex_table[0x80 | ((c >> 6) & 0x3f)] + hex_table[0x80 | (c & 0x3f)]; + continue; + } + + i += 1; + c = 0x10000 + (((c & 0x3ff) << 10) | (segment.charCodeAt(i) & 0x3ff)); + + arr[arr.length] = + hex_table[0xf0 | (c >> 18)]! + + hex_table[0x80 | ((c >> 12) & 0x3f)] + + hex_table[0x80 | ((c >> 6) & 0x3f)] + + hex_table[0x80 | (c & 0x3f)]; + } + + out += arr.join(''); + } + + return out; +}; + +export function compact(value: any) { + const queue = [{ obj: { o: value }, prop: 'o' }]; + const refs = []; + + for (let i = 0; i < queue.length; ++i) { + const item = queue[i]; + // @ts-ignore + const obj = item.obj[item.prop]; + + const keys = Object.keys(obj); + for (let j = 0; j < keys.length; ++j) { + const key = keys[j]!; + const val = obj[key]; + if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { + queue.push({ obj: obj, prop: key }); + refs.push(val); + } + } + } + + compact_queue(queue); + + return value; +} + +export function is_regexp(obj: any) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +} + +export function is_buffer(obj: any) { + if (!obj || typeof obj !== 'object') { + return false; + } + + return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); +} + +export function combine(a: any, b: any) { + return [].concat(a, b); +} + +export function maybe_map(val: T[], fn: (v: T) => T) { + if (isArray(val)) { + const mapped = []; + for (let i = 0; i < val.length; i += 1) { + mapped.push(fn(val[i]!)); + } + return mapped; + } + return fn(val); +} diff --git a/src/resources/accounts.ts b/src/resources/accounts.ts index 6691161..dd879d6 100644 --- a/src/resources/accounts.ts +++ b/src/resources/accounts.ts @@ -2,11 +2,26 @@ import { APIResource } from '../core/resource'; import * as Shared from './shared'; +import { APIPromise } from '../core/api-promise'; +import { RequestOptions } from '../internal/request-options'; /** * Accounts operations */ -export class Accounts extends APIResource {} +export class Accounts extends APIResource { + /** + * Lists chat accounts across networks (WhatsApp, Telegram, Twitter/X, etc.) + * actively connected to this Beeper Desktop instance + * + * @example + * ```ts + * const accounts = await client.accounts.list(); + * ``` + */ + list(options?: RequestOptions): APIPromise { + return this._client.get('/v1/accounts', options); + } +} /** * A chat account added to Beeper @@ -18,18 +33,22 @@ export interface Account { accountID: string; /** - * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You - * MUST use 'accountID' to perform actions. + * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). */ network: string; /** - * A person on or reachable through Beeper. Values are best-effort and can vary by - * network. + * User the account belongs to. */ user: Shared.User; } +/** + * Connected accounts the user can act through. Includes accountID, network, and + * user identity. + */ +export type AccountListResponse = Array; + export declare namespace Accounts { - export { type Account as Account }; + export { type Account as Account, type AccountListResponse as AccountListResponse }; } diff --git a/src/resources/chats/chats.ts b/src/resources/chats/chats.ts index b4218f0..b4bc923 100644 --- a/src/resources/chats/chats.ts +++ b/src/resources/chats/chats.ts @@ -3,15 +3,95 @@ import { APIResource } from '../../core/resource'; import * as Shared from '../shared'; import * as RemindersAPI from './reminders'; -import { Reminders } from './reminders'; +import { ReminderCreateParams, Reminders } from './reminders'; +import { APIPromise } from '../../core/api-promise'; +import { Cursor, type CursorParams, PagePromise } from '../../core/pagination'; +import { RequestOptions } from '../../internal/request-options'; +import { path } from '../../internal/utils/path'; /** * Chats operations */ export class Chats extends APIResource { reminders: RemindersAPI.Reminders = new RemindersAPI.Reminders(this._client); + + /** + * Create a single or group chat on a specific account using participant IDs and + * optional title. + * + * @example + * ```ts + * const chat = await client.chats.create({ + * accountID: + * 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + * participantIDs: ['string'], + * type: 'single', + * }); + * ``` + */ + create(body: ChatCreateParams, options?: RequestOptions): APIPromise { + return this._client.post('/v1/chats', { body, ...options }); + } + + /** + * Retrieve chat details including metadata, participants, and latest message + * + * @example + * ```ts + * const chat = await client.chats.retrieve( + * '!NCdzlIaMjZUmvmvyHU:beeper.com', + * ); + * ``` + */ + retrieve( + chatID: string, + query: ChatRetrieveParams | null | undefined = {}, + options?: RequestOptions, + ): APIPromise { + return this._client.get(path`/v1/chats/${chatID}`, { 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( + * '!NCdzlIaMjZUmvmvyHU:beeper.com', + * ); + * ``` + */ + archive( + chatID: string, + body: ChatArchiveParams | null | undefined = {}, + options?: RequestOptions, + ): APIPromise { + return this._client.post(path`/v1/chats/${chatID}/archive`, { body, ...options }); + } + + /** + * Search chats by title/network or participants using Beeper Desktop's renderer + * algorithm. + * + * @example + * ```ts + * // Automatically fetches more pages as needed. + * for await (const chat of client.chats.search()) { + * // ... + * } + * ``` + */ + search( + query: ChatSearchParams | null | undefined = {}, + options?: RequestOptions, + ): PagePromise { + return this._client.getAPIList('/v1/chats/search', Cursor, { query, ...options }); + } } +export type ChatsCursor = Cursor; + export interface Chat { /** * Unique identifier of the chat (room/thread ID, same as id) across Beeper. @@ -24,8 +104,7 @@ export interface Chat { accountID: string; /** - * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). You - * MUST use 'accountID' to perform actions. + * Display-only human-readable network name (e.g., 'WhatsApp', 'Messenger'). */ network: string; @@ -103,10 +182,123 @@ export namespace Chat { } } +export interface ChatCreateResponse extends Shared.BaseResponse { + /** + * Newly created chat if available. + */ + chatID?: string; +} + +export interface ChatCreateParams { + /** + * Account to create the chat on. + */ + accountID: string; + + /** + * User IDs to include in the new chat. + */ + participantIDs: Array; + + /** + * Chat type to create: 'single' requires exactly one participantID; 'group' + * supports multiple participants and optional title. + */ + type: 'single' | 'group'; + + /** + * Optional first message content if the platform requires it to create the chat. + */ + messageText?: string; + + /** + * Optional title for group chats; ignored for single chats on most platforms. + */ + title?: string; +} + +export interface ChatRetrieveParams { + /** + * Maximum number of participants to return. Use -1 for all; otherwise 0–500. + * Defaults to 20. + */ + maxParticipantCount?: number | null; +} + +export interface ChatArchiveParams { + /** + * True to archive, false to unarchive + */ + archived?: boolean; +} + +export interface ChatSearchParams extends CursorParams { + /** + * 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 | null; + + /** + * 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; + + /** + * Literal token search (non-semantic). Use single words users type (e.g., + * "dinner"). When multiple words provided, ALL must match. Case-insensitive. + */ + query?: string; + + /** + * Search scope: 'titles' matches title + network; 'participants' matches + * participant names. + */ + scope?: 'titles' | 'participants'; + + /** + * Specify the type of chats to retrieve: use "single" for direct messages, "group" + * for group chats, or "any" to get all types + */ + type?: 'single' | 'group' | 'any'; + + /** + * Set to true to only retrieve chats that have unread messages + */ + unreadOnly?: boolean | null; +} + Chats.Reminders = Reminders; export declare namespace Chats { - export { type Chat as Chat }; + export { + type Chat as Chat, + type ChatCreateResponse as ChatCreateResponse, + type ChatsCursor as ChatsCursor, + type ChatCreateParams as ChatCreateParams, + type ChatRetrieveParams as ChatRetrieveParams, + type ChatArchiveParams as ChatArchiveParams, + type ChatSearchParams as ChatSearchParams, + }; - export { Reminders as Reminders }; + export { Reminders as Reminders, type ReminderCreateParams as ReminderCreateParams }; } diff --git a/src/resources/chats/index.ts b/src/resources/chats/index.ts index deb6a20..3410cad 100644 --- a/src/resources/chats/index.ts +++ b/src/resources/chats/index.ts @@ -1,4 +1,13 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export { Chats, type Chat } from './chats'; -export { Reminders } from './reminders'; +export { + Chats, + type Chat, + type ChatCreateResponse, + type ChatCreateParams, + type ChatRetrieveParams, + type ChatArchiveParams, + type ChatSearchParams, + type ChatsCursor, +} from './chats'; +export { Reminders, type ReminderCreateParams } from './reminders'; diff --git a/src/resources/chats/reminders.ts b/src/resources/chats/reminders.ts index 4ec6691..8da0a14 100644 --- a/src/resources/chats/reminders.ts +++ b/src/resources/chats/reminders.ts @@ -1,8 +1,73 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../../core/resource'; +import * as Shared from '../shared'; +import { APIPromise } from '../../core/api-promise'; +import { RequestOptions } from '../../internal/request-options'; +import { path } from '../../internal/utils/path'; /** * Reminders operations */ -export class Reminders extends APIResource {} +export class Reminders extends APIResource { + /** + * Set a reminder for a chat at a specific time + * + * @example + * ```ts + * const baseResponse = await client.chats.reminders.create( + * '!NCdzlIaMjZUmvmvyHU:beeper.com', + * { reminder: { remindAtMs: 0 } }, + * ); + * ``` + */ + create( + chatID: string, + body: ReminderCreateParams, + options?: RequestOptions, + ): APIPromise { + return this._client.post(path`/v1/chats/${chatID}/reminders`, { body, ...options }); + } + + /** + * Clear an existing reminder from a chat + * + * @example + * ```ts + * const baseResponse = await client.chats.reminders.delete( + * '!NCdzlIaMjZUmvmvyHU:beeper.com', + * ); + * ``` + */ + delete(chatID: string, options?: RequestOptions): APIPromise { + return this._client.delete(path`/v1/chats/${chatID}/reminders`, options); + } +} + +export interface ReminderCreateParams { + /** + * Reminder configuration + */ + reminder: ReminderCreateParams.Reminder; +} + +export namespace ReminderCreateParams { + /** + * 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 ReminderCreateParams as ReminderCreateParams }; +} diff --git a/src/resources/contacts.ts b/src/resources/contacts.ts index bb9a056..3d0b682 100644 --- a/src/resources/contacts.ts +++ b/src/resources/contacts.ts @@ -1,8 +1,42 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../core/resource'; +import * as Shared from './shared'; +import { APIPromise } from '../core/api-promise'; +import { RequestOptions } from '../internal/request-options'; /** * Contacts operations */ -export class Contacts extends APIResource {} +export class Contacts extends APIResource { + /** + * Search contacts across on a specific account using the network's search API. + * Only use for creating new chats. + */ + search(query: ContactSearchParams, options?: RequestOptions): APIPromise { + return this._client.get('/v1/contacts/search', { query, ...options }); + } +} + +export interface ContactSearchResponse { + items: Array; +} + +export interface ContactSearchParams { + /** + * Account ID this resource belongs to. + */ + accountID: string; + + /** + * Text to search users by. Network-specific behavior. + */ + query: string; +} + +export declare namespace Contacts { + export { + type ContactSearchResponse as ContactSearchResponse, + type ContactSearchParams as ContactSearchParams, + }; +} diff --git a/src/resources/index.ts b/src/resources/index.ts index 4979af0..71a5a82 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -1,8 +1,30 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. export * from './shared'; -export { Accounts, type Account } from './accounts'; -export { Chats, type Chat } from './chats/chats'; -export { Contacts } from './contacts'; -export { Messages } from './messages'; +export { Accounts, type Account, type AccountListResponse } from './accounts'; +export { + Chats, + type Chat, + type ChatCreateResponse, + type ChatCreateParams, + type ChatRetrieveParams, + type ChatArchiveParams, + type ChatSearchParams, + type ChatsCursor, +} from './chats/chats'; +export { Contacts, type ContactSearchResponse, type ContactSearchParams } from './contacts'; +export { + Messages, + type MessageSendResponse, + type MessageSearchParams, + type MessageSendParams, +} from './messages'; export { Token, type UserInfo } from './token'; +export { + type DownloadAssetResponse, + type OpenResponse, + type SearchResponse, + type DownloadAssetParams, + type OpenParams, + type SearchParams, +} from './top-level'; diff --git a/src/resources/messages.ts b/src/resources/messages.ts index 9043fd3..8f1d91b 100644 --- a/src/resources/messages.ts +++ b/src/resources/messages.ts @@ -1,8 +1,146 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../core/resource'; +import * as Shared from './shared'; +import { MessagesCursor } from './shared'; +import { APIPromise } from '../core/api-promise'; +import { Cursor, type CursorParams, PagePromise } from '../core/pagination'; +import { RequestOptions } from '../internal/request-options'; /** * Messages operations */ -export class Messages extends APIResource {} +export class Messages extends APIResource { + /** + * Search messages across chats using Beeper's message index + * + * @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('/v1/messages/search', Cursor, { query, ...options }); + } + + /** + * Send a text message to a specific chat. Supports replying to existing messages. + * Returns the sent message ID. + * + * @example + * ```ts + * const response = await client.messages.send({ + * chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', + * }); + * ``` + */ + send(body: MessageSendParams, options?: RequestOptions): APIPromise { + return this._client.post('/v1/messages', { body, ...options }); + } +} + +export interface MessageSendResponse extends Shared.BaseResponse { + /** + * Unique identifier of the chat. + */ + chatID: string; + + /** + * Pending message ID + */ + pendingMessageID: string; +} + +export interface MessageSearchParams extends CursorParams { + /** + * Limit search to specific account IDs. + */ + accountIDs?: Array; + + /** + * Limit search to specific 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 | null; + + /** + * 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 | null; + + /** + * Filter messages by media types. Use ['any'] for any media type, or specify exact + * types like ['video', 'image']. Omit for no media filtering. + */ + mediaTypes?: Array<'any' | 'video' | 'image' | 'link' | 'file'>; + + /** + * 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 { + /** + * Unique identifier of the chat. + */ + 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 MessageSendResponse as MessageSendResponse, + type MessageSearchParams as MessageSearchParams, + type MessageSendParams as MessageSendParams, + }; +} + +export { type MessagesCursor }; diff --git a/src/resources/shared.ts b/src/resources/shared.ts index 8d65847..98ffebb 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -1,5 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { Cursor } from '../core/pagination'; + export interface Attachment { /** * Attachment type. @@ -78,19 +80,36 @@ export interface BaseResponse { export interface Error { /** - * Error message + * Error details */ - error: string; + error: Error.Error; +} +export namespace Error { /** - * Error code + * Error details */ - code?: string; + export interface Error { + /** + * Machine-readable error code + */ + code: string; - /** - * Additional error details - */ - details?: { [key: string]: string }; + /** + * Error message + */ + message: string; + + /** + * Error type (e.g., invalid_request_error, authentication_error, not_found_error) + */ + type: string; + + /** + * Parameter that caused the error + */ + param?: string; + } } export interface Message { @@ -192,8 +211,7 @@ export interface Reaction { } /** - * A person on or reachable through Beeper. Values are best-effort and can vary by - * network. + * User the account belongs to. */ export interface User { /** @@ -239,3 +257,5 @@ export interface User { */ username?: string; } + +export type MessagesCursor = Cursor; diff --git a/src/resources/top-level.ts b/src/resources/top-level.ts new file mode 100644 index 0000000..7a9eafe --- /dev/null +++ b/src/resources/top-level.ts @@ -0,0 +1,125 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import * as Shared from './shared'; +import * as ChatsAPI from './chats/chats'; + +export interface DownloadAssetResponse { + /** + * Error message if the download failed. + */ + error?: string; + + /** + * Local file URL to the downloaded asset. + */ + srcURL?: string; +} + +/** + * Response indicating successful app opening. + */ +export interface OpenResponse { + /** + * Whether the app was successfully opened/focused. + */ + success: boolean; +} + +export interface SearchResponse { + results: SearchResponse.Results; +} + +export namespace SearchResponse { + export interface Results { + /** + * Top chat results. + */ + chats: Array; + + /** + * Top group results by participant matches. + */ + in_groups: Array; + + messages: Results.Messages; + } + + export namespace Results { + export interface Messages { + /** + * Map of chatID -> chat details for chats referenced in items. + */ + chats: { [key: string]: ChatsAPI.Chat }; + + /** + * True if additional results can be fetched using the provided cursors. + */ + hasMore: boolean; + + /** + * Messages matching the query and filters. + */ + items: Array; + + /** + * Cursor for fetching newer results (use with direction='after'). Opaque string; + * do not inspect. + */ + newestCursor: string | null; + + /** + * Cursor for fetching older results (use with direction='before'). Opaque string; + * do not inspect. + */ + oldestCursor: string | null; + } + } +} + +export interface DownloadAssetParams { + /** + * Matrix content URL (mxc:// or localmxc://) for the asset to download. + */ + url: string; +} + +export interface OpenParams { + /** + * Optional Beeper chat ID (or local chat ID) to focus after opening the app. If + * omitted, only opens/focuses the app. + */ + chatID?: string; + + /** + * Optional draft attachment path to populate in the message input field. + */ + draftAttachmentPath?: string; + + /** + * Optional draft text to populate in the message input field. + */ + draftText?: string; + + /** + * Optional message ID. Jumps to that message in the chat when opening. + */ + messageID?: string; +} + +export interface SearchParams { + /** + * User-typed search text. Literal word matching (NOT semantic). + */ + query: string; +} + +export declare namespace TopLevel { + export { + type DownloadAssetResponse as DownloadAssetResponse, + type OpenResponse as OpenResponse, + type SearchResponse as SearchResponse, + type DownloadAssetParams as DownloadAssetParams, + type OpenParams as OpenParams, + type SearchParams as SearchParams, + }; +} diff --git a/tests/api-resources/accounts.test.ts b/tests/api-resources/accounts.test.ts new file mode 100644 index 0000000..e8917eb --- /dev/null +++ b/tests/api-resources/accounts.test.ts @@ -0,0 +1,21 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from '@beeper/desktop-api'; + +const client = new BeeperDesktop({ + accessToken: 'My Access Token', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource accounts', () => { + test('list', async () => { + const responsePromise = client.accounts.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); + }); +}); diff --git a/tests/api-resources/chats/chats.test.ts b/tests/api-resources/chats/chats.test.ts new file mode 100644 index 0000000..a054ec2 --- /dev/null +++ b/tests/api-resources/chats/chats.test.ts @@ -0,0 +1,116 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from '@beeper/desktop-api'; + +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('create: only required params', async () => { + const responsePromise = client.chats.create({ + accountID: 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + participantIDs: ['string'], + type: 'single', + }); + 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('create: required and optional params', async () => { + const response = await client.chats.create({ + accountID: 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + participantIDs: ['string'], + type: 'single', + messageText: 'messageText', + title: 'title', + }); + }); + + test('retrieve', async () => { + const responsePromise = client.chats.retrieve('!NCdzlIaMjZUmvmvyHU:beeper.com'); + 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: 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.retrieve( + '!NCdzlIaMjZUmvmvyHU:beeper.com', + { maxParticipantCount: 50 }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(BeeperDesktop.NotFoundError); + }); + + test('archive', async () => { + const responsePromise = client.chats.archive('!NCdzlIaMjZUmvmvyHU:beeper.com'); + 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: 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.archive( + '!NCdzlIaMjZUmvmvyHU:beeper.com', + { archived: true }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(BeeperDesktop.NotFoundError); + }); + + test('search', async () => { + const responsePromise = client.chats.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.chats.search( + { + accountIDs: [ + 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + 'local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI', + ], + cursor: 'eyJvZmZzZXQiOjE3MTk5OTk5OTl9', + direction: 'after', + inbox: 'primary', + includeMuted: true, + lastActivityAfter: '2019-12-27T18:11:19.117Z', + lastActivityBefore: '2019-12-27T18:11:19.117Z', + limit: 1, + query: 'x', + scope: 'titles', + type: 'single', + unreadOnly: true, + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(BeeperDesktop.NotFoundError); + }); +}); diff --git a/tests/api-resources/chats/reminders.test.ts b/tests/api-resources/chats/reminders.test.ts new file mode 100644 index 0000000..82114dc --- /dev/null +++ b/tests/api-resources/chats/reminders.test.ts @@ -0,0 +1,40 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from '@beeper/desktop-api'; + +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('create: only required params', async () => { + const responsePromise = client.chats.reminders.create('!NCdzlIaMjZUmvmvyHU:beeper.com', { + 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('create: required and optional params', async () => { + const response = await client.chats.reminders.create('!NCdzlIaMjZUmvmvyHU:beeper.com', { + reminder: { remindAtMs: 0, dismissOnIncomingMessage: true }, + }); + }); + + test('delete', async () => { + const responsePromise = client.chats.reminders.delete('!NCdzlIaMjZUmvmvyHU:beeper.com'); + 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); + }); +}); diff --git a/tests/api-resources/contacts.test.ts b/tests/api-resources/contacts.test.ts new file mode 100644 index 0000000..b21cda7 --- /dev/null +++ b/tests/api-resources/contacts.test.ts @@ -0,0 +1,31 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from '@beeper/desktop-api'; + +const client = new BeeperDesktop({ + accessToken: 'My Access Token', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource contacts', () => { + test('search: only required params', async () => { + const responsePromise = client.contacts.search({ + accountID: 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + query: 'x', + }); + 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: required and optional params', async () => { + const response = await client.contacts.search({ + accountID: 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + query: 'x', + }); + }); +}); diff --git a/tests/api-resources/messages.test.ts b/tests/api-resources/messages.test.ts new file mode 100644 index 0000000..49910d6 --- /dev/null +++ b/tests/api-resources/messages.test.ts @@ -0,0 +1,68 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from '@beeper/desktop-api'; + +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('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: [ + 'whatsapp', + 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + 'local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU', + ], + chatIDs: ['!NCdzlIaMjZUmvmvyHU:beeper.com', '1231073'], + chatType: 'group', + cursor: '1725489123456|c29tZUltc2dQYWdl', + dateAfter: '2025-08-01T00:00:00Z', + dateBefore: '2025-08-31T23:59:59Z', + direction: 'before', + excludeLowPriority: true, + includeMuted: true, + limit: 20, + mediaTypes: ['any'], + query: 'dinner', + sender: 'me', + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(BeeperDesktop.NotFoundError); + }); + + test('send: only required params', async () => { + const responsePromise = client.messages.send({ chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com' }); + 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: '!NCdzlIaMjZUmvmvyHU:beeper.com', + replyToMessageID: 'replyToMessageID', + text: 'text', + }); + }); +}); diff --git a/tests/api-resources/top-level.test.ts b/tests/api-resources/top-level.test.ts new file mode 100644 index 0000000..9ad90a9 --- /dev/null +++ b/tests/api-resources/top-level.test.ts @@ -0,0 +1,66 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import BeeperDesktop from '@beeper/desktop-api'; + +const client = new BeeperDesktop({ + accessToken: 'My Access Token', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('top level methods', () => { + test('downloadAsset: only required params', async () => { + const responsePromise = client.downloadAsset({ url: 'mxc://example.org/Q4x9CqGz1pB3Oa6XgJ' }); + 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('downloadAsset: required and optional params', async () => { + const response = await client.downloadAsset({ url: 'mxc://example.org/Q4x9CqGz1pB3Oa6XgJ' }); + }); + + test('open', async () => { + const responsePromise = client.open(); + 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('open: 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.open( + { + chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', + draftAttachmentPath: 'draftAttachmentPath', + draftText: 'draftText', + messageID: 'messageID', + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(BeeperDesktop.NotFoundError); + }); + + test('search: only required params', async () => { + const responsePromise = client.search({ query: 'x' }); + 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: required and optional params', async () => { + const response = await client.search({ query: 'x' }); + }); +}); diff --git a/tests/qs/empty-keys-cases.ts b/tests/qs/empty-keys-cases.ts new file mode 100644 index 0000000..ea2c1b0 --- /dev/null +++ b/tests/qs/empty-keys-cases.ts @@ -0,0 +1,271 @@ +export const empty_test_cases = [ + { + input: '&', + with_empty_keys: {}, + stringify_output: { + brackets: '', + indices: '', + repeat: '', + }, + no_empty_keys: {}, + }, + { + input: '&&', + with_empty_keys: {}, + stringify_output: { + brackets: '', + indices: '', + repeat: '', + }, + no_empty_keys: {}, + }, + { + input: '&=', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '&=&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '&=&=', + with_empty_keys: { '': ['', ''] }, + stringify_output: { + brackets: '[]=&[]=', + indices: '[0]=&[1]=', + repeat: '=&=', + }, + no_empty_keys: {}, + }, + { + input: '&=&=&', + with_empty_keys: { '': ['', ''] }, + stringify_output: { + brackets: '[]=&[]=', + indices: '[0]=&[1]=', + repeat: '=&=', + }, + no_empty_keys: {}, + }, + { + input: '=', + with_empty_keys: { '': '' }, + no_empty_keys: {}, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + }, + { + input: '=&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '=&&&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '=&=&=&', + with_empty_keys: { '': ['', '', ''] }, + stringify_output: { + brackets: '[]=&[]=&[]=', + indices: '[0]=&[1]=&[2]=', + repeat: '=&=&=', + }, + no_empty_keys: {}, + }, + { + input: '=&a[]=b&a[1]=c', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: '=a', + with_empty_keys: { '': 'a' }, + no_empty_keys: {}, + stringify_output: { + brackets: '=a', + indices: '=a', + repeat: '=a', + }, + }, + { + input: 'a==a', + with_empty_keys: { a: '=a' }, + no_empty_keys: { a: '=a' }, + stringify_output: { + brackets: 'a==a', + indices: 'a==a', + repeat: 'a==a', + }, + }, + { + input: '=&a[]=b', + with_empty_keys: { '': '', a: ['b'] }, + stringify_output: { + brackets: '=&a[]=b', + indices: '=&a[0]=b', + repeat: '=&a=b', + }, + no_empty_keys: { a: ['b'] }, + }, + { + input: '=&a[]=b&a[]=c&a[2]=d', + with_empty_keys: { '': '', a: ['b', 'c', 'd'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c&a[]=d', + indices: '=&a[0]=b&a[1]=c&a[2]=d', + repeat: '=&a=b&a=c&a=d', + }, + no_empty_keys: { a: ['b', 'c', 'd'] }, + }, + { + input: '=a&=b', + with_empty_keys: { '': ['a', 'b'] }, + stringify_output: { + brackets: '[]=a&[]=b', + indices: '[0]=a&[1]=b', + repeat: '=a&=b', + }, + no_empty_keys: {}, + }, + { + input: '=a&foo=b', + with_empty_keys: { '': 'a', foo: 'b' }, + no_empty_keys: { foo: 'b' }, + stringify_output: { + brackets: '=a&foo=b', + indices: '=a&foo=b', + repeat: '=a&foo=b', + }, + }, + { + input: 'a[]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a[]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a[0]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a=b&a[]=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a=b&a[0]=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: '[]=a&[]=b& []=1', + with_empty_keys: { '': ['a', 'b'], ' ': ['1'] }, + stringify_output: { + brackets: '[]=a&[]=b& []=1', + indices: '[0]=a&[1]=b& [0]=1', + repeat: '=a&=b& =1', + }, + no_empty_keys: { 0: 'a', 1: 'b', ' ': ['1'] }, + }, + { + input: '[0]=a&[1]=b&a[0]=1&a[1]=2', + with_empty_keys: { '': ['a', 'b'], a: ['1', '2'] }, + no_empty_keys: { 0: 'a', 1: 'b', a: ['1', '2'] }, + stringify_output: { + brackets: '[]=a&[]=b&a[]=1&a[]=2', + indices: '[0]=a&[1]=b&a[0]=1&a[1]=2', + repeat: '=a&=b&a=1&a=2', + }, + }, + { + input: '[deep]=a&[deep]=2', + with_empty_keys: { '': { deep: ['a', '2'] } }, + stringify_output: { + brackets: '[deep][]=a&[deep][]=2', + indices: '[deep][0]=a&[deep][1]=2', + repeat: '[deep]=a&[deep]=2', + }, + no_empty_keys: { deep: ['a', '2'] }, + }, + { + input: '%5B0%5D=a&%5B1%5D=b', + with_empty_keys: { '': ['a', 'b'] }, + stringify_output: { + brackets: '[]=a&[]=b', + indices: '[0]=a&[1]=b', + repeat: '=a&=b', + }, + no_empty_keys: { 0: 'a', 1: 'b' }, + }, +] satisfies { + input: string; + with_empty_keys: Record; + stringify_output: { + brackets: string; + indices: string; + repeat: string; + }; + no_empty_keys: Record; +}[]; diff --git a/tests/qs/stringify.test.ts b/tests/qs/stringify.test.ts new file mode 100644 index 0000000..d9e9393 --- /dev/null +++ b/tests/qs/stringify.test.ts @@ -0,0 +1,2232 @@ +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 { empty_test_cases } from './empty-keys-cases'; +import assert from 'assert'; + +describe('stringify()', function () { + test('stringifies a querystring object', function () { + expect(stringify({ a: 'b' })).toBe('a=b'); + expect(stringify({ a: 1 })).toBe('a=1'); + expect(stringify({ a: 1, b: 2 })).toBe('a=1&b=2'); + expect(stringify({ a: 'A_Z' })).toBe('a=A_Z'); + expect(stringify({ a: '€' })).toBe('a=%E2%82%AC'); + expect(stringify({ a: '' })).toBe('a=%EE%80%80'); + expect(stringify({ a: 'א' })).toBe('a=%D7%90'); + expect(stringify({ a: '𐐷' })).toBe('a=%F0%90%90%B7'); + }); + + test('stringifies falsy values', function () { + expect(stringify(undefined)).toBe(''); + expect(stringify(null)).toBe(''); + expect(stringify(null, { strictNullHandling: true })).toBe(''); + expect(stringify(false)).toBe(''); + expect(stringify(0)).toBe(''); + }); + + test('stringifies symbols', function () { + expect(stringify(Symbol.iterator)).toBe(''); + expect(stringify([Symbol.iterator])).toBe('0=Symbol%28Symbol.iterator%29'); + expect(stringify({ a: Symbol.iterator })).toBe('a=Symbol%28Symbol.iterator%29'); + expect(stringify({ a: [Symbol.iterator] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=Symbol%28Symbol.iterator%29', + ); + }); + + test('stringifies bigints', function () { + var three = BigInt(3); + // @ts-expect-error + var encodeWithN = function (value, defaultEncoder, charset) { + var result = defaultEncoder(value, defaultEncoder, charset); + return typeof value === 'bigint' ? result + 'n' : result; + }; + + expect(stringify(three)).toBe(''); + expect(stringify([three])).toBe('0=3'); + expect(stringify([three], { encoder: encodeWithN })).toBe('0=3n'); + expect(stringify({ a: three })).toBe('a=3'); + expect(stringify({ a: three }, { encoder: encodeWithN })).toBe('a=3n'); + expect(stringify({ a: [three] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a[]=3'); + expect( + stringify({ a: [three] }, { encodeValuesOnly: true, encoder: encodeWithN, arrayFormat: 'brackets' }), + ).toBe('a[]=3n'); + }); + + test('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function () { + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: false, encodeDotInKeys: false }), + ).toBe('name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: false }), + ).toBe('name.obj.first=John&name.obj.last=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: false, encodeDotInKeys: true }), + ).toBe('name%252Eobj%5Bfirst%5D=John&name%252Eobj%5Blast%5D=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true }), + ).toBe('name%252Eobj.first=John&name%252Eobj.last=Doe'); + + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: false, encodeDotInKeys: false }, + // ), + // 'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe', + // 'with allowDots false and encodeDotInKeys false', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: false }, + // ), + // 'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe', + // 'with allowDots false and encodeDotInKeys false', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: false, encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe', + // 'with allowDots false and encodeDotInKeys true', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe', + // 'with allowDots true and encodeDotInKeys true', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: false, encodeDotInKeys: false }, + ), + ).toBe('name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: false }, + ), + ).toBe('name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: false, encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe'); + }); + + test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function () { + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe', + // 'with allowDots undefined and encodeDotInKeys true', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe'); + }); + + test('should encode dot in key of object when encodeDotInKeys and allowDots is provided, and nothing else when encodeValuesOnly is provided', function () { + // st.equal( + // stringify( + // { 'name.obj': { first: 'John', last: 'Doe' } }, + // { + // encodeDotInKeys: true, + // allowDots: true, + // encodeValuesOnly: true, + // }, + // ), + // 'name%2Eobj.first=John&name%2Eobj.last=Doe', + // ); + expect( + stringify( + { 'name.obj': { first: 'John', last: 'Doe' } }, + { + encodeDotInKeys: true, + allowDots: true, + encodeValuesOnly: true, + }, + ), + ).toBe('name%2Eobj.first=John&name%2Eobj.last=Doe'); + + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }, + // ), + // 'name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }, + ), + ).toBe('name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe'); + }); + + test('throws when `commaRoundTrip` is not a boolean', function () { + // st['throws']( + // function () { + // stringify({}, { commaRoundTrip: 'not a boolean' }); + // }, + // TypeError, + // 'throws when `commaRoundTrip` is not a boolean', + // ); + expect(() => { + // @ts-expect-error + stringify({}, { commaRoundTrip: 'not a boolean' }); + }).toThrow(TypeError); + }); + + test('throws when `encodeDotInKeys` is not a boolean', function () { + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); + }).toThrow(TypeError); + }); + + test('adds query prefix', function () { + // st.equal(stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b'); + expect(stringify({ a: 'b' }, { addQueryPrefix: true })).toBe('?a=b'); + }); + + test('with query prefix, outputs blank string given an empty object', function () { + // st.equal(stringify({}, { addQueryPrefix: true }), ''); + expect(stringify({}, { addQueryPrefix: true })).toBe(''); + }); + + test('stringifies nested falsy values', function () { + // st.equal(stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D='); + // st.equal( + // stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), + // 'a%5Bb%5D%5Bc%5D', + // ); + // st.equal(stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false'); + expect(stringify({ a: { b: { c: null } } })).toBe('a%5Bb%5D%5Bc%5D='); + expect(stringify({ a: { b: { c: null } } }, { strictNullHandling: true })).toBe('a%5Bb%5D%5Bc%5D'); + expect(stringify({ a: { b: { c: false } } })).toBe('a%5Bb%5D%5Bc%5D=false'); + }); + + test('stringifies a nested object', function () { + // st.equal(stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); + // st.equal(stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e'); + expect(stringify({ a: { b: 'c' } })).toBe('a%5Bb%5D=c'); + expect(stringify({ a: { b: { c: { d: 'e' } } } })).toBe('a%5Bb%5D%5Bc%5D%5Bd%5D=e'); + }); + + test('`allowDots` option: stringifies a nested object with dots notation', function () { + // st.equal(stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c'); + // st.equal(stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e'); + expect(stringify({ a: { b: 'c' } }, { allowDots: true })).toBe('a.b=c'); + expect(stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true })).toBe('a.b.c.d=e'); + }); + + test('stringifies an array value', function () { + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }), + // 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }), + // 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' }), + // 'a=b%2Cc%2Cd', + // 'comma => comma', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true }), + // 'a=b%2Cc%2Cd', + // 'comma round trip => comma', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }), + // 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + // 'default => indices', + // ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' })).toBe( + 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' })).toBe( + 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d', + ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' })).toBe('a=b%2Cc%2Cd'); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true })).toBe( + 'a=b%2Cc%2Cd', + ); + expect(stringify({ a: ['b', 'c', 'd'] })).toBe('a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d'); + }); + + test('`skipNulls` option', function () { + // st.equal( + // stringify({ a: 'b', c: null }, { skipNulls: true }), + // 'a=b', + // 'omits nulls when asked', + // ); + expect(stringify({ a: 'b', c: null }, { skipNulls: true })).toBe('a=b'); + + // st.equal( + // stringify({ a: { b: 'c', d: null } }, { skipNulls: true }), + // 'a%5Bb%5D=c', + // 'omits nested nulls when asked', + // ); + expect(stringify({ a: { b: 'c', d: null } }, { skipNulls: true })).toBe('a%5Bb%5D=c'); + }); + + test('omits array indices when asked', function () { + // st.equal(stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d'); + expect(stringify({ a: ['b', 'c', 'd'] }, { indices: false })).toBe('a=b&a=c&a=d'); + }); + + test('omits object key/value pair when value is empty array', function () { + // st.equal(stringify({ a: [], b: 'zz' }), 'b=zz'); + expect(stringify({ a: [], b: 'zz' })).toBe('b=zz'); + }); + + test('should not omit object key/value pair when value is empty array and when asked', function () { + // st.equal(stringify({ a: [], b: 'zz' }), 'b=zz'); + // st.equal(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz'); + // st.equal(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz'); + expect(stringify({ a: [], b: 'zz' })).toBe('b=zz'); + expect(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false })).toBe('b=zz'); + expect(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true })).toBe('a[]&b=zz'); + }); + + test('should throw when allowEmptyArrays is not of type boolean', function () { + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); + }).toThrow(TypeError); + }); + + test('allowEmptyArrays + strictNullHandling', function () { + // st.equal( + // stringify({ testEmptyArray: [] }, { strictNullHandling: true, allowEmptyArrays: true }), + // 'testEmptyArray[]', + // ); + expect(stringify({ testEmptyArray: [] }, { strictNullHandling: true, allowEmptyArrays: true })).toBe( + 'testEmptyArray[]', + ); + }); + + describe('stringifies an array value with one item vs multiple items', function () { + test('non-array item', function () { + // s2t.equal( + // stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a=c', + // ); + // s2t.equal( + // stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a=c', + // ); + // s2t.equal(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c'); + // s2t.equal(stringify({ a: 'c' }, { encodeValuesOnly: true }), 'a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true })).toBe('a=c'); + }); + + test('array with a single item', function () { + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0]=c', + // ); + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=c', + // ); + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c', + // ); + // s2t.equal( + // stringify( + // { a: ['c'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a[]=c', + // ); // so it parses back as an array + // s2t.equal(stringify({ a: ['c'] }, { encodeValuesOnly: true }), 'a[0]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe('a[0]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a[]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c'); + expect( + stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('a[]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true })).toBe('a[0]=c'); + }); + + test('array with multiple items', function () { + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0]=c&a[1]=d', + // ); + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=c&a[]=d', + // ); + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c,d', + // ); + // s2t.equal( + // stringify( + // { a: ['c', 'd'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a=c,d', + // ); + // s2t.equal(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true }), 'a[0]=c&a[1]=d'); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[0]=c&a[1]=d', + ); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=c&a[]=d', + ); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c,d'); + expect( + stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('a=c,d'); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true })).toBe('a[0]=c&a[1]=d'); + }); + + test('array with multiple items with a comma inside', function () { + // s2t.equal( + // stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c%2Cd,e', + // ); + // s2t.equal(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' }), 'a=c%2Cd%2Ce'); + expect(stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe( + 'a=c%2Cd,e', + ); + expect(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' })).toBe('a=c%2Cd%2Ce'); + + // s2t.equal( + // stringify( + // { a: ['c,d', 'e'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a=c%2Cd,e', + // ); + // s2t.equal( + // stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true }), + // 'a=c%2Cd%2Ce', + // ); + expect( + stringify( + { a: ['c,d', 'e'] }, + { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + ), + ).toBe('a=c%2Cd,e'); + expect(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true })).toBe( + 'a=c%2Cd%2Ce', + ); + }); + }); + + test('stringifies a nested array value', function () { + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[b][0]=c&a[b][1]=d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[b][]=c&a[b][]=d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe( + 'a[b]=c,d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true })).toBe('a[b][0]=c&a[b][1]=d'); + }); + + test('stringifies comma and empty array values', function () { + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' }), + // 'a[0]=,&a[1]=&a[2]=c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' }), + // 'a[]=,&a[]=&a[]=c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' }), + // 'a=,,,c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' }), + // 'a=,&a=&a=c,d%', + // ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' })).toBe( + 'a[0]=,&a[1]=&a[2]=c,d%', + ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'a[]=,&a[]=&a[]=c,d%', + ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' })).toBe('a=,,,c,d%'); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' })).toBe( + 'a=,&a=&a=c,d%', + ); + + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[0]=%2C&a[1]=&a[2]=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[]=%2C&a[]=&a[]=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a=%2C,,c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&a=&a=c%2Cd%25', + // ); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), + ).toBe('a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[]=%2C&a[]=&a[]=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), + ).toBe('a=%2C%2C%2Cc%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + // ), + // 'a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + // ), + // 'a%5B%5D=%2C&a%5B%5D=&a%5B%5D=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + // ), + // 'a=%2C%2C%2Cc%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&a=&a=c%2Cd%25', + // ); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), + ).toBe('a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[]=%2C&a[]=&a[]=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), + ).toBe('a=%2C%2C%2Cc%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + }); + + test('stringifies comma and empty non-array values', function () { + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'comma' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'repeat' }), + // 'a=,&b=&c=c,d%', + // ); + expect(stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' })).toBe( + 'a=,&b=&c=c,d%', + ); + expect(stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'a=,&b=&c=c,d%', + ); + + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + }); + + test('stringifies a nested array value with dots notation', function () { + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a.b[0]=c&a.b[1]=d', + // 'indices: stringifies with dots + indices', + // ); + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a.b[]=c&a.b[]=d', + // 'brackets: stringifies with dots + brackets', + // ); + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a.b=c,d', + // 'comma: stringifies with dots + comma', + // ); + // st.equal( + // stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true }), + // 'a.b[0]=c&a.b[1]=d', + // 'default: stringifies with dots + indices', + // ); + expect( + stringify( + { a: { b: ['c', 'd'] } }, + { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a.b[0]=c&a.b[1]=d'); + expect( + stringify( + { a: { b: ['c', 'd'] } }, + { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a.b[]=c&a.b[]=d'); + expect( + stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }), + ).toBe('a.b=c,d'); + expect(stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true })).toBe( + 'a.b[0]=c&a.b[1]=d', + ); + }); + + test('stringifies an object inside an array', function () { + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true }), + // 'a[0][b]=c', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }), + // 'a[b]=c', + // 'repeat => repeat', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }), + // 'a[][b]=c', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true }), + // 'a[0][b]=c', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true })).toBe( + 'a[0][b]=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true })).toBe('a[b]=c'); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true })).toBe( + 'a[][b]=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true })).toBe('a[0][b]=c'); + + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true }), + // 'a[0][b][c][0]=1', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }), + // 'a[b][c]=1', + // 'repeat => repeat', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }), + // 'a[][b][c][]=1', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true }), + // 'a[0][b][c][0]=1', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true })).toBe( + 'a[0][b][c][0]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true })).toBe( + 'a[b][c]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true })).toBe( + 'a[][b][c][]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true })).toBe('a[0][b][c][0]=1'); + }); + + test('stringifies an array with mixed objects and primitives', function () { + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0][b]=1&a[1]=2&a[2]=3', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[][b]=1&a[]=2&a[]=3', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // '???', + // 'brackets => brackets', + // { skip: 'TODO: figure out what this should do' }, + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true }), + // 'a[0][b]=1&a[1]=2&a[2]=3', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[0][b]=1&a[1]=2&a[2]=3', + ); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[][b]=1&a[]=2&a[]=3', + ); + // !Skipped: Figure out what this should do + // expect( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // ).toBe('???'); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true })).toBe('a[0][b]=1&a[1]=2&a[2]=3'); + }); + + test('stringifies an object inside an array with dots notation', function () { + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'indices' }), + // 'a[0].b=c', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: [{ b: 'c' }] }, + // { allowDots: true, encode: false, arrayFormat: 'brackets' }, + // ), + // 'a[].b=c', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false }), + // 'a[0].b=c', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'indices' })).toBe( + 'a[0].b=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'brackets' })).toBe( + 'a[].b=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false })).toBe('a[0].b=c'); + + // st.equal( + // stringify( + // { a: [{ b: { c: [1] } }] }, + // { allowDots: true, encode: false, arrayFormat: 'indices' }, + // ), + // 'a[0].b.c[0]=1', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: [{ b: { c: [1] } }] }, + // { allowDots: true, encode: false, arrayFormat: 'brackets' }, + // ), + // 'a[].b.c[]=1', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false }), + // 'a[0].b.c[0]=1', + // 'default => indices', + // ); + expect( + stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false, arrayFormat: 'indices' }), + ).toBe('a[0].b.c[0]=1'); + expect( + stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false, arrayFormat: 'brackets' }), + ).toBe('a[].b.c[]=1'); + expect(stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false })).toBe('a[0].b.c[0]=1'); + }); + + test('does not omit object keys when indices = false', function () { + // st.equal(stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c'); + expect(stringify({ a: [{ b: 'c' }] }, { indices: false })).toBe('a%5Bb%5D=c'); + }); + + test('uses indices notation for arrays when indices=true', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { indices: true })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses indices notation for arrays when no arrayFormat is specified', function () { + // st.equal(stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses indices notation for arrays when arrayFormat=indices', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses repeat notation for arrays when arrayFormat=repeat', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })).toBe('a=b&a=c'); + }); + + test('uses brackets notation for arrays when arrayFormat=brackets', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })).toBe('a%5B%5D=b&a%5B%5D=c'); + }); + + test('stringifies a complicated object', function () { + // st.equal(stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e'); + expect(stringify({ a: { b: 'c', d: 'e' } })).toBe('a%5Bb%5D=c&a%5Bd%5D=e'); + }); + + test('stringifies an empty value', function () { + // st.equal(stringify({ a: '' }), 'a='); + // st.equal(stringify({ a: null }, { strictNullHandling: true }), 'a'); + expect(stringify({ a: '' })).toBe('a='); + expect(stringify({ a: null }, { strictNullHandling: true })).toBe('a'); + + // st.equal(stringify({ a: '', b: '' }), 'a=&b='); + // st.equal(stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b='); + expect(stringify({ a: '', b: '' })).toBe('a=&b='); + expect(stringify({ a: null, b: '' }, { strictNullHandling: true })).toBe('a&b='); + + // st.equal(stringify({ a: { b: '' } }), 'a%5Bb%5D='); + // st.equal(stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D'); + // st.equal(stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D='); + expect(stringify({ a: { b: '' } })).toBe('a%5Bb%5D='); + expect(stringify({ a: { b: null } }, { strictNullHandling: true })).toBe('a%5Bb%5D'); + expect(stringify({ a: { b: null } }, { strictNullHandling: false })).toBe('a%5Bb%5D='); + }); + + test('stringifies an empty array in different arrayFormat', function () { + // st.equal(stringify({ a: [], b: [null], c: 'c' }, { encode: false }), 'b[0]=&c=c'); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false })).toBe('b[0]=&c=c'); + // arrayFormat default + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' }), + // 'b[0]=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' }), + // 'b[]=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' }), + // 'b=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' }), + // 'b=&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'b[]=&c=c', + // ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' })).toBe( + 'b[0]=&c=c', + ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'b[]=&c=c', + ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' })).toBe('b=&c=c'); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' })).toBe('b=&c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('b[]=&c=c'); + + // with strictNullHandling + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'indices', strictNullHandling: true }, + // ), + // 'b[0]&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'brackets', strictNullHandling: true }, + // ), + // 'b[]&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'repeat', strictNullHandling: true }, + // ), + // 'b&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', strictNullHandling: true }, + // ), + // 'b&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }, + // ), + // 'b[]&c=c', + // ); + + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'indices', strictNullHandling: true }, + ), + ).toBe('b[0]&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'brackets', strictNullHandling: true }, + ), + ).toBe('b[]&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'repeat', strictNullHandling: true }, + ), + ).toBe('b&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'comma', strictNullHandling: true }, + ), + ).toBe('b&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }, + ), + ).toBe('b[]&c=c'); + + // with skipNulls + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'indices', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'brackets', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'repeat', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', skipNulls: true }, + // ), + // 'c=c', + // ); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', skipNulls: true }), + ).toBe('c=c'); + }); + + test('stringifies a null object', function () { + var obj = Object.create(null); + obj.a = 'b'; + // st.equal(stringify(obj), 'a=b'); + expect(stringify(obj)).toBe('a=b'); + }); + + test('returns an empty string for invalid input', function () { + // st.equal(stringify(undefined), ''); + // st.equal(stringify(false), ''); + // st.equal(stringify(null), ''); + // st.equal(stringify(''), ''); + expect(stringify(undefined)).toBe(''); + expect(stringify(false)).toBe(''); + expect(stringify(null)).toBe(''); + expect(stringify('')).toBe(''); + }); + + test('stringifies an object with a null object as a child', function () { + var obj = { a: Object.create(null) }; + + obj.a.b = 'c'; + // st.equal(stringify(obj), 'a%5Bb%5D=c'); + expect(stringify(obj)).toBe('a%5Bb%5D=c'); + }); + + test('drops keys with a value of undefined', function () { + // st.equal(stringify({ a: undefined }), ''); + expect(stringify({ a: undefined })).toBe(''); + + // st.equal( + // stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), + // 'a%5Bc%5D', + // ); + // st.equal( + // stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), + // 'a%5Bc%5D=', + // ); + // st.equal(stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D='); + expect(stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true })).toBe('a%5Bc%5D'); + expect(stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false })).toBe('a%5Bc%5D='); + expect(stringify({ a: { b: undefined, c: '' } })).toBe('a%5Bc%5D='); + }); + + test('url encodes values', function () { + // st.equal(stringify({ a: 'b c' }), 'a=b%20c'); + expect(stringify({ a: 'b c' })).toBe('a=b%20c'); + }); + + test('stringifies a date', function () { + var now = new Date(); + var str = 'a=' + encodeURIComponent(now.toISOString()); + // st.equal(stringify({ a: now }), str); + expect(stringify({ a: now })).toBe(str); + }); + + test('stringifies the weird object from qs', function () { + // st.equal( + // stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), + // 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F', + // ); + expect(stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' })).toBe( + 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F', + ); + }); + + // TODO: Investigate how to to intercept in vitest + // TODO(rob) + test('skips properties that are part of the object prototype', function () { + // st.intercept(Object.prototype, 'crash', { value: 'test' }); + // @ts-expect-error + Object.prototype.crash = 'test'; + + // st.equal(stringify({ a: 'b' }), 'a=b'); + // st.equal(stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); + expect(stringify({ a: 'b' })).toBe('a=b'); + expect(stringify({ a: { b: 'c' } })).toBe('a%5Bb%5D=c'); + }); + + test('stringifies boolean values', function () { + // st.equal(stringify({ a: true }), 'a=true'); + // st.equal(stringify({ a: { b: true } }), 'a%5Bb%5D=true'); + // st.equal(stringify({ b: false }), 'b=false'); + // st.equal(stringify({ b: { c: false } }), 'b%5Bc%5D=false'); + expect(stringify({ a: true })).toBe('a=true'); + expect(stringify({ a: { b: true } })).toBe('a%5Bb%5D=true'); + expect(stringify({ b: false })).toBe('b=false'); + expect(stringify({ b: { c: false } })).toBe('b%5Bc%5D=false'); + }); + + test('stringifies buffer values', function () { + // st.equal(stringify({ a: Buffer.from('test') }), 'a=test'); + // st.equal(stringify({ a: { b: Buffer.from('test') } }), 'a%5Bb%5D=test'); + }); + + test('stringifies an object using an alternative delimiter', function () { + // st.equal(stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d'); + expect(stringify({ a: 'b', c: 'd' }, { delimiter: ';' })).toBe('a=b;c=d'); + }); + + // We dont target environments which dont even have Buffer + // test('does not blow up when Buffer global is missing', function () { + // var restore = mockProperty(global, 'Buffer', { delete: true }); + + // var result = stringify({ a: 'b', c: 'd' }); + + // restore(); + + // st.equal(result, 'a=b&c=d'); + // st.end(); + // }); + + test('does not crash when parsing circular references', function () { + var a: any = {}; + a.b = a; + + // st['throws']( + // function () { + // stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); + // }, + // /RangeError: Cyclic object value/, + // 'cyclic values throw', + // ); + expect(() => { + stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); + }).toThrow('Cyclic object value'); + + var circular: any = { + a: 'value', + }; + circular.a = circular; + // st['throws']( + // function () { + // stringify(circular); + // }, + // /RangeError: Cyclic object value/, + // 'cyclic values throw', + // ); + expect(() => { + stringify(circular); + }).toThrow('Cyclic object value'); + + var arr = ['a']; + // st.doesNotThrow(function () { + // stringify({ x: arr, y: arr }); + // }, 'non-cyclic values do not throw'); + expect(() => { + stringify({ x: arr, y: arr }); + }).not.toThrow(); + }); + + test('non-circular duplicated references can still work', function () { + var hourOfDay = { + function: 'hour_of_day', + }; + + var p1 = { + function: 'gte', + arguments: [hourOfDay, 0], + }; + var p2 = { + function: 'lte', + arguments: [hourOfDay, 23], + }; + + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23', + // ); + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23', + // ); + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23', + // ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe( + 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23', + ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe( + 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23', + ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe( + 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23', + ); + }); + + test('selects properties when filter=array', function () { + // st.equal(stringify({ a: 'b' }, { filter: ['a'] }), 'a=b'); + // st.equal(stringify({ a: 1 }, { filter: [] }), ''); + expect(stringify({ a: 'b' }, { filter: ['a'] })).toBe('a=b'); + expect(stringify({ a: 1 }, { filter: [] })).toBe(''); + + // st.equal( + // stringify( + // { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + // { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }, + // ), + // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + // { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }, + // ), + // 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] }), + // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + // 'default => indices', + // ); + expect(stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] })).toBe( + 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + ); + expect( + stringify( + { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }, + ), + ).toBe('a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3'); + expect( + stringify( + { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }, + ), + ).toBe('a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3'); + }); + + test('supports custom representations when filter=function', function () { + var calls = 0; + var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } }; + var filterFunc: StringifyOptions['filter'] = function (prefix, value) { + calls += 1; + if (calls === 1) { + // st.equal(prefix, '', 'prefix is empty'); + // st.equal(value, obj); + expect(prefix).toBe(''); + expect(value).toBe(obj); + } else if (prefix === 'c') { + return void 0; + } else if (value instanceof Date) { + // st.equal(prefix, 'e[f]'); + expect(prefix).toBe('e[f]'); + return value.getTime(); + } + return value; + }; + + // st.equal(stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000'); + // st.equal(calls, 5); + expect(stringify(obj, { filter: filterFunc })).toBe('a=b&e%5Bf%5D=1257894000000'); + expect(calls).toBe(5); + }); + + test('can disable uri encoding', function () { + // st.equal(stringify({ a: 'b' }, { encode: false }), 'a=b'); + // st.equal(stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c'); + // st.equal( + // stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), + // 'a=b&c', + // ); + expect(stringify({ a: 'b' }, { encode: false })).toBe('a=b'); + expect(stringify({ a: { b: 'c' } }, { encode: false })).toBe('a[b]=c'); + expect(stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false })).toBe('a=b&c'); + }); + + test('can sort the keys', function () { + // @ts-expect-error + var sort: NonNullable = function (a: string, b: string) { + return a.localeCompare(b); + }; + // st.equal(stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y'); + // st.equal( + // stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), + // 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a', + // ); + expect(stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort })).toBe('a=c&b=f&z=y'); + expect(stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort })).toBe( + 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a', + ); + }); + + test('can sort the keys at depth 3 or more too', function () { + // @ts-expect-error + var sort: NonNullable = function (a: string, b: string) { + return a.localeCompare(b); + }; + // st.equal( + // stringify( + // { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + // { sort: sort, encode: false }, + // ), + // 'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb', + // ); + // st.equal( + // stringify( + // { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + // { sort: null, encode: false }, + // ), + // 'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b', + // ); + expect( + stringify( + { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + { sort: sort, encode: false }, + ), + ).toBe('a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'); + expect( + stringify( + { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + { sort: null, encode: false }, + ), + ).toBe('a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'); + }); + + test('can stringify with custom encoding', function () { + // st.equal( + // stringify( + // { 県: '大阪府', '': '' }, + // { + // encoder: function (str) { + // if (str.length === 0) { + // return ''; + // } + // var buf = iconv.encode(str, 'shiftjis'); + // var result = []; + // for (var i = 0; i < buf.length; ++i) { + // result.push(buf.readUInt8(i).toString(16)); + // } + // return '%' + result.join('%'); + // }, + // }, + // ), + // '%8c%a7=%91%e5%8d%e3%95%7b&=', + // ); + expect( + stringify( + { 県: '大阪府', '': '' }, + { + encoder: function (str) { + if (str.length === 0) { + return ''; + } + var buf = iconv.encode(str, 'shiftjis'); + var result = []; + for (var i = 0; i < buf.length; ++i) { + result.push(buf.readUInt8(i).toString(16)); + } + return '%' + result.join('%'); + }, + }, + ), + ).toBe('%8c%a7=%91%e5%8d%e3%95%7b&='); + }); + + test('receives the default encoder as a second argument', function () { + // stringify( + // { a: 1, b: new Date(), c: true, d: [1] }, + // { + // encoder: function (str) { + // st.match(typeof str, /^(?:string|number|boolean)$/); + // return ''; + // }, + // }, + // ); + + stringify( + { a: 1, b: new Date(), c: true, d: [1] }, + { + encoder: function (str) { + // st.match(typeof str, /^(?:string|number|boolean)$/); + assert.match(typeof str, /^(?:string|number|boolean)$/); + return ''; + }, + }, + ); + }); + + test('receives the default encoder as a second argument', function () { + // stringify( + // { a: 1 }, + // { + // encoder: function (str, defaultEncoder) { + // st.equal(defaultEncoder, utils.encode); + // }, + // }, + // ); + + stringify( + { a: 1 }, + { + // @ts-ignore + encoder: function (_str, defaultEncoder) { + expect(defaultEncoder).toBe(encode); + }, + }, + ); + }); + + test('throws error with wrong encoder', function () { + // st['throws'](function () { + // stringify({}, { encoder: 'string' }); + // }, new TypeError('Encoder has to be a function.')); + // st.end(); + expect(() => { + // @ts-expect-error + stringify({}, { encoder: 'string' }); + }).toThrow(TypeError); + }); + + (typeof Buffer === 'undefined' ? test.skip : test)( + 'can use custom encoder for a buffer object', + function () { + // st.equal( + // stringify( + // { a: Buffer.from([1]) }, + // { + // encoder: function (buffer) { + // if (typeof buffer === 'string') { + // return buffer; + // } + // return String.fromCharCode(buffer.readUInt8(0) + 97); + // }, + // }, + // ), + // 'a=b', + // ); + expect( + stringify( + { a: Buffer.from([1]) }, + { + encoder: function (buffer) { + if (typeof buffer === 'string') { + return buffer; + } + return String.fromCharCode(buffer.readUInt8(0) + 97); + }, + }, + ), + ).toBe('a=b'); + + // st.equal( + // stringify( + // { a: Buffer.from('a b') }, + // { + // encoder: function (buffer) { + // return buffer; + // }, + // }, + // ), + // 'a=a b', + // ); + expect( + stringify( + { a: Buffer.from('a b') }, + { + encoder: function (buffer) { + return buffer; + }, + }, + ), + ).toBe('a=a b'); + }, + ); + + test('serializeDate option', function () { + var date = new Date(); + // st.equal( + // stringify({ a: date }), + // 'a=' + date.toISOString().replace(/:/g, '%3A'), + // 'default is toISOString', + // ); + expect(stringify({ a: date })).toBe('a=' + date.toISOString().replace(/:/g, '%3A')); + + var mutatedDate = new Date(); + mutatedDate.toISOString = function () { + throw new SyntaxError(); + }; + // st['throws'](function () { + // mutatedDate.toISOString(); + // }, SyntaxError); + expect(() => { + mutatedDate.toISOString(); + }).toThrow(SyntaxError); + // st.equal( + // stringify({ a: mutatedDate }), + // 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'), + // 'toISOString works even when method is not locally present', + // ); + expect(stringify({ a: mutatedDate })).toBe( + 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'), + ); + + var specificDate = new Date(6); + // st.equal( + // stringify( + // { a: specificDate }, + // { + // serializeDate: function (d) { + // return d.getTime() * 7; + // }, + // }, + // ), + // 'a=42', + // 'custom serializeDate function called', + // ); + expect( + stringify( + { a: specificDate }, + { + // @ts-ignore + serializeDate: function (d) { + return d.getTime() * 7; + }, + }, + ), + ).toBe('a=42'); + + // st.equal( + // stringify( + // { a: [date] }, + // { + // serializeDate: function (d) { + // return d.getTime(); + // }, + // arrayFormat: 'comma', + // }, + // ), + // 'a=' + date.getTime(), + // 'works with arrayFormat comma', + // ); + // st.equal( + // stringify( + // { a: [date] }, + // { + // serializeDate: function (d) { + // return d.getTime(); + // }, + // arrayFormat: 'comma', + // commaRoundTrip: true, + // }, + // ), + // 'a%5B%5D=' + date.getTime(), + // 'works with arrayFormat comma', + // ); + expect( + stringify( + { a: [date] }, + { + // @ts-expect-error + serializeDate: function (d) { + return d.getTime(); + }, + arrayFormat: 'comma', + }, + ), + ).toBe('a=' + date.getTime()); + expect( + stringify( + { a: [date] }, + { + // @ts-expect-error + serializeDate: function (d) { + return d.getTime(); + }, + arrayFormat: 'comma', + commaRoundTrip: true, + }, + ), + ).toBe('a%5B%5D=' + date.getTime()); + }); + + test('RFC 1738 serialization', function () { + // st.equal(stringify({ a: 'b c' }, { format: formats.RFC1738 }), 'a=b+c'); + // st.equal(stringify({ 'a b': 'c d' }, { format: formats.RFC1738 }), 'a+b=c+d'); + // st.equal( + // stringify({ 'a b': Buffer.from('a b') }, { format: formats.RFC1738 }), + // 'a+b=a+b', + // ); + expect(stringify({ a: 'b c' }, { format: 'RFC1738' })).toBe('a=b+c'); + expect(stringify({ 'a b': 'c d' }, { format: 'RFC1738' })).toBe('a+b=c+d'); + expect(stringify({ 'a b': Buffer.from('a b') }, { format: 'RFC1738' })).toBe('a+b=a+b'); + + // st.equal(stringify({ 'foo(ref)': 'bar' }, { format: formats.RFC1738 }), 'foo(ref)=bar'); + expect(stringify({ 'foo(ref)': 'bar' }, { format: 'RFC1738' })).toBe('foo(ref)=bar'); + }); + + test('RFC 3986 spaces serialization', function () { + // st.equal(stringify({ a: 'b c' }, { format: formats.RFC3986 }), 'a=b%20c'); + // st.equal(stringify({ 'a b': 'c d' }, { format: formats.RFC3986 }), 'a%20b=c%20d'); + // st.equal( + // stringify({ 'a b': Buffer.from('a b') }, { format: formats.RFC3986 }), + // 'a%20b=a%20b', + // ); + expect(stringify({ a: 'b c' }, { format: 'RFC3986' })).toBe('a=b%20c'); + expect(stringify({ 'a b': 'c d' }, { format: 'RFC3986' })).toBe('a%20b=c%20d'); + expect(stringify({ 'a b': Buffer.from('a b') }, { format: 'RFC3986' })).toBe('a%20b=a%20b'); + }); + + test('Backward compatibility to RFC 3986', function () { + // st.equal(stringify({ a: 'b c' }), 'a=b%20c'); + // st.equal(stringify({ 'a b': Buffer.from('a b') }), 'a%20b=a%20b'); + expect(stringify({ a: 'b c' })).toBe('a=b%20c'); + expect(stringify({ 'a b': Buffer.from('a b') })).toBe('a%20b=a%20b'); + }); + + test('Edge cases and unknown formats', function () { + ['UFO1234', false, 1234, null, {}, []].forEach(function (format) { + // st['throws'](function () { + // stringify({ a: 'b c' }, { format: format }); + // }, new TypeError('Unknown format option provided.')); + expect(() => { + // @ts-expect-error + stringify({ a: 'b c' }, { format: format }); + }).toThrow(TypeError); + }); + }); + + test('encodeValuesOnly', function () { + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h', + // 'encodeValuesOnly + indices', + // ); + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h', + // 'encodeValuesOnly + brackets', + // ); + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=b&c=d&c=e%3Df&f=g&f=h', + // 'encodeValuesOnly + repeat', + // ); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h'); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'repeat' }, + ), + ).toBe('a=b&c=d&c=e%3Df&f=g&f=h'); + + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'indices' }), + // 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h', + // 'no encodeValuesOnly + indices', + // ); + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'brackets' }), + // 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h', + // 'no encodeValuesOnly + brackets', + // ); + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'repeat' }), + // 'a=b&c=d&c=e&f=g&f=h', + // 'no encodeValuesOnly + repeat', + // ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'indices' })).toBe( + 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h', + ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'brackets' })).toBe( + 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h', + ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'repeat' })).toBe( + 'a=b&c=d&c=e&f=g&f=h', + ); + }); + + test('encodeValuesOnly - strictNullHandling', function () { + // st.equal( + // stringify({ a: { b: null } }, { encodeValuesOnly: true, strictNullHandling: true }), + // 'a[b]', + // ); + expect(stringify({ a: { b: null } }, { encodeValuesOnly: true, strictNullHandling: true })).toBe('a[b]'); + }); + + test('throws if an invalid charset is specified', function () { + // st['throws'](function () { + // stringify({ a: 'b' }, { charset: 'foobar' }); + // }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined')); + expect(() => { + // @ts-expect-error + stringify({ a: 'b' }, { charset: 'foobar' }); + }).toThrow(TypeError); + }); + + test('respects a charset of iso-8859-1', function () { + // st.equal(stringify({ æ: 'æ' }, { charset: 'iso-8859-1' }), '%E6=%E6'); + expect(stringify({ æ: 'æ' }, { charset: 'iso-8859-1' })).toBe('%E6=%E6'); + }); + + test('encodes unrepresentable chars as numeric entities in iso-8859-1 mode', function () { + // st.equal(stringify({ a: '☺' }, { charset: 'iso-8859-1' }), 'a=%26%239786%3B'); + expect(stringify({ a: '☺' }, { charset: 'iso-8859-1' })).toBe('a=%26%239786%3B'); + }); + + test('respects an explicit charset of utf-8 (the default)', function () { + // st.equal(stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6'); + expect(stringify({ a: 'æ' }, { charset: 'utf-8' })).toBe('a=%C3%A6'); + }); + + test('`charsetSentinel` option', function () { + // st.equal( + // stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }), + // 'utf8=%E2%9C%93&a=%C3%A6', + // 'adds the right sentinel when instructed to and the charset is utf-8', + // ); + expect(stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' })).toBe( + 'utf8=%E2%9C%93&a=%C3%A6', + ); + + // st.equal( + // stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }), + // 'utf8=%26%2310003%3B&a=%E6', + // 'adds the right sentinel when instructed to and the charset is iso-8859-1', + // ); + expect(stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' })).toBe( + 'utf8=%26%2310003%3B&a=%E6', + ); + }); + + test('does not mutate the options argument', function () { + var options = {}; + stringify({}, options); + // st.deepEqual(options, {}); + expect(options).toEqual({}); + }); + + test('strictNullHandling works with custom filter', function () { + // @ts-expect-error + var filter = function (_prefix, value) { + return value; + }; + + var options = { strictNullHandling: true, filter: filter }; + // st.equal(stringify({ key: null }, options), 'key'); + expect(stringify({ key: null }, options)).toBe('key'); + }); + + test('strictNullHandling works with null serializeDate', function () { + var serializeDate = function () { + return null; + }; + var options = { strictNullHandling: true, serializeDate: serializeDate }; + var date = new Date(); + // st.equal(stringify({ key: date }, options), 'key'); + // @ts-expect-error + expect(stringify({ key: date }, options)).toBe('key'); + }); + + test('allows for encoding keys and values differently', function () { + // @ts-expect-error + var encoder = function (str, defaultEncoder, charset, type) { + if (type === 'key') { + return defaultEncoder(str, defaultEncoder, charset, type).toLowerCase(); + } + if (type === 'value') { + return defaultEncoder(str, defaultEncoder, charset, type).toUpperCase(); + } + throw 'this should never happen! type: ' + type; + }; + + // st.deepEqual(stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE'); + expect(stringify({ KeY: 'vAlUe' }, { encoder: encoder })).toBe('key=VALUE'); + }); + + test('objects inside arrays', function () { + var obj = { a: { b: { c: 'd', e: 'f' } } }; + var withArray = { a: { b: [{ c: 'd', e: 'f' }] } }; + + // st.equal( + // stringify(obj, { encode: false }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, no arrayFormat', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'brackets' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, bracket', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'indices' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, indices', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'repeat' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, repeat', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'comma' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, comma', + // ); + expect(stringify(obj, { encode: false })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'brackets' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'indices' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'repeat' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'comma' })).toBe('a[b][c]=d&a[b][e]=f'); + + // st.equal( + // stringify(withArray, { encode: false }), + // 'a[b][0][c]=d&a[b][0][e]=f', + // 'array, no arrayFormat', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'brackets' }), + // 'a[b][][c]=d&a[b][][e]=f', + // 'array, bracket', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'indices' }), + // 'a[b][0][c]=d&a[b][0][e]=f', + // 'array, indices', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'repeat' }), + // 'a[b][c]=d&a[b][e]=f', + // 'array, repeat', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'comma' }), + // '???', + // 'array, comma', + // { skip: 'TODO: figure out what this should do' }, + // ); + expect(stringify(withArray, { encode: false })).toBe('a[b][0][c]=d&a[b][0][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'brackets' })).toBe('a[b][][c]=d&a[b][][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'indices' })).toBe('a[b][0][c]=d&a[b][0][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'repeat' })).toBe('a[b][c]=d&a[b][e]=f'); + // !TODo: Figure out what this should do + // expect(stringify(withArray, { encode: false, arrayFormat: 'comma' })).toBe( + // 'a[b][c]=d&a[b][e]=f', + // ); + }); + + test('stringifies sparse arrays', function () { + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[1]=2&a[4]=1', + // ); + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=2&a[]=1', + // ); + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + // 'a=2&a=1', + // ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[1]=2&a[4]=1', + ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=2&a[]=1', + ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' })).toBe( + 'a=2&a=1', + ); + + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][b][2][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][b][][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[b][c]=1', + // ); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][b][2][c]=1'); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][b][][c]=1'); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[b][c]=1'); + + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][2][3][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][][][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[c]=1', + // ); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][2][3][c]=1'); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][][][c]=1'); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[c]=1'); + + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][2][3][c][1]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][][][c][]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[c]=1', + // ); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][2][3][c][1]=1'); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][][][c][]=1'); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[c]=1'); + }); + + test('encodes a very long string', function () { + var chars = []; + var expected = []; + for (var i = 0; i < 5e3; i++) { + chars.push(' ' + i); + + expected.push('%20' + i); + } + + var obj = { + foo: chars.join(''), + }; + + // st.equal( + // stringify(obj, { arrayFormat: 'bracket', charset: 'utf-8' }), + // 'foo=' + expected.join(''), + // ); + // @ts-expect-error + expect(stringify(obj, { arrayFormat: 'bracket', charset: 'utf-8' })).toBe('foo=' + expected.join('')); + }); +}); + +describe('stringifies empty keys', function () { + empty_test_cases.forEach(function (testCase) { + test('stringifies an object with empty string key with ' + testCase.input, function () { + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'indices' }), + // testCase.stringifyOutput.indices, + // 'test case: ' + testCase.input + ', indices', + // ); + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'brackets' }), + // testCase.stringifyOutput.brackets, + // 'test case: ' + testCase.input + ', brackets', + // ); + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'repeat' }), + // testCase.stringifyOutput.repeat, + // 'test case: ' + testCase.input + ', repeat', + // ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'indices' })).toBe( + testCase.stringify_output.indices, + ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'brackets' })).toBe( + testCase.stringify_output.brackets, + ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'repeat' })).toBe( + testCase.stringify_output.repeat, + ); + }); + }); + + test('edge case with object/arrays', function () { + // st.deepEqual(stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3'); + // st.deepEqual( + // stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), + // '[][0]=2&[][1]=3&[a]=2', + // ); + // st.deepEqual( + // stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' }), + // '[][0]=2&[][1]=3', + // ); + // st.deepEqual( + // stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' }), + // '[][0]=2&[][1]=3&[a]=2', + // ); + expect(stringify({ '': { '': [2, 3] } }, { encode: false })).toBe('[][0]=2&[][1]=3'); + expect(stringify({ '': { '': [2, 3], a: 2 } }, { encode: false })).toBe('[][0]=2&[][1]=3&[a]=2'); + expect(stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' })).toBe( + '[][0]=2&[][1]=3', + ); + expect(stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' })).toBe( + '[][0]=2&[][1]=3&[a]=2', + ); + }); +}); diff --git a/tests/qs/utils.test.ts b/tests/qs/utils.test.ts new file mode 100644 index 0000000..80b2700 --- /dev/null +++ b/tests/qs/utils.test.ts @@ -0,0 +1,169 @@ +import { combine, merge, is_buffer, assign_single_source } from '@beeper/desktop-api/internal/qs/utils'; + +describe('merge()', function () { + // t.deepEqual(merge(null, true), [null, true], 'merges true into null'); + expect(merge(null, true)).toEqual([null, true]); + + // t.deepEqual(merge(null, [42]), [null, 42], 'merges null into an array'); + expect(merge(null, [42])).toEqual([null, 42]); + + // t.deepEqual( + // merge({ a: 'b' }, { a: 'c' }), + // { a: ['b', 'c'] }, + // 'merges two objects with the same key', + // ); + expect(merge({ a: 'b' }, { a: 'c' })).toEqual({ a: ['b', 'c'] }); + + var oneMerged = merge({ foo: 'bar' }, { foo: { first: '123' } }); + // t.deepEqual( + // oneMerged, + // { foo: ['bar', { first: '123' }] }, + // 'merges a standalone and an object into an array', + // ); + expect(oneMerged).toEqual({ foo: ['bar', { first: '123' }] }); + + var twoMerged = merge({ foo: ['bar', { first: '123' }] }, { foo: { second: '456' } }); + // t.deepEqual( + // twoMerged, + // { foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }, + // 'merges a standalone and two objects into an array', + // ); + expect(twoMerged).toEqual({ foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }); + + var sandwiched = merge({ foo: ['bar', { first: '123', second: '456' }] }, { foo: 'baz' }); + // t.deepEqual( + // sandwiched, + // { foo: ['bar', { first: '123', second: '456' }, 'baz'] }, + // 'merges an object sandwiched by two standalones into an array', + // ); + expect(sandwiched).toEqual({ foo: ['bar', { first: '123', second: '456' }, 'baz'] }); + + var nestedArrays = merge({ foo: ['baz'] }, { foo: ['bar', 'xyzzy'] }); + // t.deepEqual(nestedArrays, { foo: ['baz', 'bar', 'xyzzy'] }); + expect(nestedArrays).toEqual({ foo: ['baz', 'bar', 'xyzzy'] }); + + var noOptionsNonObjectSource = merge({ foo: 'baz' }, 'bar'); + // t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true }); + expect(noOptionsNonObjectSource).toEqual({ foo: 'baz', bar: true }); + + (typeof Object.defineProperty !== 'function' ? test.skip : test)( + 'avoids invoking array setters unnecessarily', + function () { + var setCount = 0; + var getCount = 0; + var observed: any[] = []; + Object.defineProperty(observed, 0, { + get: function () { + getCount += 1; + return { bar: 'baz' }; + }, + set: function () { + setCount += 1; + }, + }); + merge(observed, [null]); + // st.equal(setCount, 0); + // st.equal(getCount, 1); + expect(setCount).toEqual(0); + expect(getCount).toEqual(1); + observed[0] = observed[0]; + // st.equal(setCount, 1); + // st.equal(getCount, 2); + expect(setCount).toEqual(1); + expect(getCount).toEqual(2); + }, + ); +}); + +test('assign()', function () { + var target = { a: 1, b: 2 }; + var source = { b: 3, c: 4 }; + var result = assign_single_source(target, source); + + expect(result).toEqual(target); + expect(target).toEqual({ a: 1, b: 3, c: 4 }); + expect(source).toEqual({ b: 3, c: 4 }); +}); + +describe('combine()', function () { + test('both arrays', function () { + var a = [1]; + var b = [2]; + var combined = combine(a, b); + + // st.deepEqual(a, [1], 'a is not mutated'); + // st.deepEqual(b, [2], 'b is not mutated'); + // st.notEqual(a, combined, 'a !== combined'); + // st.notEqual(b, combined, 'b !== combined'); + // st.deepEqual(combined, [1, 2], 'combined is a + b'); + expect(a).toEqual([1]); + expect(b).toEqual([2]); + expect(combined).toEqual([1, 2]); + expect(a).not.toEqual(combined); + expect(b).not.toEqual(combined); + }); + + test('one array, one non-array', function () { + var aN = 1; + var a = [aN]; + var bN = 2; + var b = [bN]; + + var combinedAnB = combine(aN, b); + // st.deepEqual(b, [bN], 'b is not mutated'); + // st.notEqual(aN, combinedAnB, 'aN + b !== aN'); + // st.notEqual(a, combinedAnB, 'aN + b !== a'); + // st.notEqual(bN, combinedAnB, 'aN + b !== bN'); + // st.notEqual(b, combinedAnB, 'aN + b !== b'); + // st.deepEqual([1, 2], combinedAnB, 'first argument is array-wrapped when not an array'); + expect(b).toEqual([bN]); + expect(combinedAnB).not.toEqual(aN); + expect(combinedAnB).not.toEqual(a); + expect(combinedAnB).not.toEqual(bN); + expect(combinedAnB).not.toEqual(b); + expect(combinedAnB).toEqual([1, 2]); + + var combinedABn = combine(a, bN); + // st.deepEqual(a, [aN], 'a is not mutated'); + // st.notEqual(aN, combinedABn, 'a + bN !== aN'); + // st.notEqual(a, combinedABn, 'a + bN !== a'); + // st.notEqual(bN, combinedABn, 'a + bN !== bN'); + // st.notEqual(b, combinedABn, 'a + bN !== b'); + // st.deepEqual([1, 2], combinedABn, 'second argument is array-wrapped when not an array'); + expect(a).toEqual([aN]); + expect(combinedABn).not.toEqual(aN); + expect(combinedABn).not.toEqual(a); + expect(combinedABn).not.toEqual(bN); + expect(combinedABn).not.toEqual(b); + expect(combinedABn).toEqual([1, 2]); + }); + + test('neither is an array', function () { + var combined = combine(1, 2); + // st.notEqual(1, combined, '1 + 2 !== 1'); + // st.notEqual(2, combined, '1 + 2 !== 2'); + // st.deepEqual([1, 2], combined, 'both arguments are array-wrapped when not an array'); + expect(combined).not.toEqual(1); + expect(combined).not.toEqual(2); + expect(combined).toEqual([1, 2]); + }); +}); + +test('is_buffer()', function () { + for (const x of [null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], function () {}, /a/g]) { + // t.equal(is_buffer(x), false, inspect(x) + ' is not a buffer'); + expect(is_buffer(x)).toEqual(false); + } + + var fakeBuffer = { constructor: Buffer }; + // t.equal(is_buffer(fakeBuffer), false, 'fake buffer is not a buffer'); + expect(is_buffer(fakeBuffer)).toEqual(false); + + var saferBuffer = Buffer.from('abc'); + // t.equal(is_buffer(saferBuffer), true, 'SaferBuffer instance is a buffer'); + expect(is_buffer(saferBuffer)).toEqual(true); + + var buffer = Buffer.from('abc'); + // t.equal(is_buffer(buffer), true, 'real Buffer instance is a buffer'); + expect(is_buffer(buffer)).toEqual(true); +}); diff --git a/tests/stringifyQuery.test.ts b/tests/stringifyQuery.test.ts index 964129f..1714856 100644 --- a/tests/stringifyQuery.test.ts +++ b/tests/stringifyQuery.test.ts @@ -20,10 +20,4 @@ describe(stringifyQuery, () => { expect(stringifyQuery(input)).toEqual(expected); }); } - - for (const value of [[], {}, new Date()]) { - it(`${JSON.stringify(value)} -> `, () => { - expect(() => stringifyQuery({ value })).toThrow(`Cannot stringify type ${typeof value}`); - }); - } }); From e404d7ebd08830f115f7468598cc275db637d508 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:02:09 +0000 Subject: [PATCH 25/36] feat(api): manual updates --- .stats.yml | 4 +- api.md | 3 + packages/mcp-server/README.md | 4 +- .../src/tools/accounts/get-accounts.ts | 2 +- .../mcp-server/src/tools/chats/list-chats.ts | 61 +++++++++++++++++++ packages/mcp-server/src/tools/index.ts | 4 ++ .../src/tools/messages/list-messages.ts | 55 +++++++++++++++++ src/client.ts | 17 +++++- src/resources/accounts.ts | 2 +- src/resources/chats/chats.ts | 38 ++++++++++++ src/resources/chats/index.ts | 3 + src/resources/index.ts | 4 ++ src/resources/messages.ts | 25 ++++++++ tests/api-resources/chats/chats.test.ts | 30 +++++++++ tests/api-resources/messages.test.ts | 20 ++++++ 15 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 packages/mcp-server/src/tools/chats/list-chats.ts create mode 100644 packages/mcp-server/src/tools/messages/list-messages.ts diff --git a/.stats.yml b/.stats.yml index 8526f3e..e531b74 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 14 +configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: f83b2b6eb86f2dd68101065998479cb2 +config_hash: be3f3b31e322be0f4de6a23e32ab004c diff --git a/api.md b/api.md index ab0991c..1647294 100644 --- a/api.md +++ b/api.md @@ -50,11 +50,13 @@ Types: - Chat - ChatCreateResponse +- ChatListResponse Methods: - client.chats.create({ ...params }) -> ChatCreateResponse - client.chats.retrieve(chatID, { ...params }) -> Chat +- client.chats.list({ ...params }) -> ChatListResponsesCursor - client.chats.archive(chatID, { ...params }) -> BaseResponse - client.chats.search({ ...params }) -> ChatsCursor @@ -73,6 +75,7 @@ Types: Methods: +- client.messages.list({ ...params }) -> MessagesCursor - client.messages.search({ ...params }) -> MessagesCursor - client.messages.send({ ...params }) -> MessageSendResponse diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index ad90c59..1f28201 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -220,7 +220,7 @@ The following tools are available in this MCP server. ### Resource `accounts`: -- `get_accounts` (`read`) tags: [accounts]: List connected accounts on this device. Use to pick account context. +- `get_accounts` (`read`) tags: [accounts]: List connected accounts on this device. ### Resource `contacts`: @@ -230,6 +230,7 @@ The following tools are available in this MCP server. - `create_chats` (`write`): Create a single or group chat on a specific account using participant IDs and optional title. - `get_chat` (`read`) tags: [chats]: Get chat details: metadata, participants (limited), last activity. +- `list_chats` (`read`): List all chats sorted by last activity (most recent first). Combines all accounts into a single paginated list. - `archive_chat` (`write`) tags: [chats]: Archive or unarchive a chat. - `search_chats` (`read`) tags: [chats]: Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'. @@ -240,6 +241,7 @@ The following tools are available in this MCP server. ### Resource `messages`: +- `list_messages` (`read`): List all messages in a chat with cursor-based pagination. Sorted by timestamp. - `search_messages` (`read`) tags: [messages]: 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. diff --git a/packages/mcp-server/src/tools/accounts/get-accounts.ts b/packages/mcp-server/src/tools/accounts/get-accounts.ts index c9de4de..ad88134 100644 --- a/packages/mcp-server/src/tools/accounts/get-accounts.ts +++ b/packages/mcp-server/src/tools/accounts/get-accounts.ts @@ -16,7 +16,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'get_accounts', - description: 'List connected accounts on this device. Use to pick account context.', + description: 'List connected accounts on this device.', inputSchema: { type: 'object', properties: {}, diff --git a/packages/mcp-server/src/tools/chats/list-chats.ts b/packages/mcp-server/src/tools/chats/list-chats.ts new file mode 100644 index 0000000..5013aa0 --- /dev/null +++ b/packages/mcp-server/src/tools/chats/list-chats.ts @@ -0,0 +1,61 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'chats', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/v1/chats', + operationId: 'listChats', +}; + +export const tool: Tool = { + name: 'list_chats', + description: + 'List all chats sorted by last activity (most recent first). Combines all accounts into a single paginated list.', + inputSchema: { + type: 'object', + properties: { + accountIDs: { + type: 'array', + description: 'Limit to specific account IDs. If omitted, fetches from all accounts.', + items: { + type: 'string', + description: 'Account ID this resource belongs to.', + }, + }, + cursor: { + type: 'string', + description: + 'Timestamp cursor (milliseconds since epoch) for pagination. Use with direction to navigate results.', + }, + direction: { + type: 'string', + description: + "Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided.", + enum: ['after', 'before'], + }, + limit: { + type: 'integer', + description: 'Maximum number of chats to return (1–200). Defaults to 50.', + }, + }, + required: [], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + const response = await client.chats.list(body).asResponse(); + return asTextContentResult(await response.json()); +}; + +export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index d35d250..f8bc786 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -11,10 +11,12 @@ import get_accounts from './accounts/get-accounts'; import search_contacts from './contacts/search-contacts'; import create_chats from './chats/create-chats'; import get_chat from './chats/get-chat'; +import list_chats from './chats/list-chats'; import archive_chat from './chats/archive-chat'; import search_chats from './chats/search-chats'; import set_chat_reminder from './chats/reminders/set-chat-reminder'; import clear_chat_reminder from './chats/reminders/clear-chat-reminder'; +import list_messages from './messages/list-messages'; import search_messages from './messages/search-messages'; import send_message from './messages/send-message'; import info_token from './token/info-token'; @@ -32,10 +34,12 @@ addEndpoint(get_accounts); addEndpoint(search_contacts); addEndpoint(create_chats); addEndpoint(get_chat); +addEndpoint(list_chats); addEndpoint(archive_chat); addEndpoint(search_chats); addEndpoint(set_chat_reminder); addEndpoint(clear_chat_reminder); +addEndpoint(list_messages); addEndpoint(search_messages); addEndpoint(send_message); addEndpoint(info_token); diff --git a/packages/mcp-server/src/tools/messages/list-messages.ts b/packages/mcp-server/src/tools/messages/list-messages.ts new file mode 100644 index 0000000..91d7c62 --- /dev/null +++ b/packages/mcp-server/src/tools/messages/list-messages.ts @@ -0,0 +1,55 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import BeeperDesktop from '@beeper/desktop-api'; + +export const metadata: Metadata = { + resource: 'messages', + operation: 'read', + tags: [], + httpMethod: 'get', + httpPath: '/v1/messages', + operationId: 'listMessages', +}; + +export const tool: Tool = { + name: 'list_messages', + description: 'List all messages in a chat with cursor-based pagination. Sorted by timestamp.', + inputSchema: { + type: 'object', + properties: { + chatID: { + type: 'string', + description: 'The chat ID to list messages from', + }, + cursor: { + type: 'string', + description: 'Message cursor for pagination. Use with direction to navigate results.', + }, + direction: { + type: 'string', + description: + "Pagination direction used with 'cursor': 'before' fetches older messages, 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is provided.", + enum: ['after', 'before'], + }, + limit: { + type: 'integer', + description: 'Maximum number of messages to return (1–500). Defaults to 50.', + }, + }, + required: ['chatID'], + }, + annotations: { + readOnlyHint: true, + }, +}; + +export const handler = async (client: BeeperDesktop, args: Record | undefined) => { + const body = args as any; + const response = await client.messages.list(body).asResponse(); + return asTextContentResult(await response.json()); +}; + +export default { metadata, tool, handler }; diff --git a/src/client.ts b/src/client.ts index 8e93db2..b5049ef 100644 --- a/src/client.ts +++ b/src/client.ts @@ -30,13 +30,22 @@ import { import { APIPromise } from './core/api-promise'; import { Account, AccountListResponse, Accounts } from './resources/accounts'; import { ContactSearchParams, ContactSearchResponse, Contacts } from './resources/contacts'; -import { MessageSearchParams, MessageSendParams, MessageSendResponse, Messages } from './resources/messages'; +import { + MessageListParams, + MessageSearchParams, + MessageSendParams, + MessageSendResponse, + Messages, +} from './resources/messages'; import { Token, UserInfo } from './resources/token'; import { Chat, ChatArchiveParams, ChatCreateParams, ChatCreateResponse, + ChatListParams, + ChatListResponse, + ChatListResponsesCursor, ChatRetrieveParams, ChatSearchParams, Chats, @@ -805,7 +814,7 @@ export class BeeperDesktop { static toFile = Uploads.toFile; /** - * Accounts operations + * Manage connected chat accounts */ accounts: API.Accounts = new API.Accounts(this); /** @@ -859,9 +868,12 @@ export declare namespace BeeperDesktop { Chats as Chats, type Chat as Chat, type ChatCreateResponse as ChatCreateResponse, + type ChatListResponse as ChatListResponse, + type ChatListResponsesCursor as ChatListResponsesCursor, type ChatsCursor as ChatsCursor, type ChatCreateParams as ChatCreateParams, type ChatRetrieveParams as ChatRetrieveParams, + type ChatListParams as ChatListParams, type ChatArchiveParams as ChatArchiveParams, type ChatSearchParams as ChatSearchParams, }; @@ -869,6 +881,7 @@ export declare namespace BeeperDesktop { export { Messages as Messages, type MessageSendResponse as MessageSendResponse, + type MessageListParams as MessageListParams, type MessageSearchParams as MessageSearchParams, type MessageSendParams as MessageSendParams, }; diff --git a/src/resources/accounts.ts b/src/resources/accounts.ts index dd879d6..9d1df07 100644 --- a/src/resources/accounts.ts +++ b/src/resources/accounts.ts @@ -6,7 +6,7 @@ import { APIPromise } from '../core/api-promise'; import { RequestOptions } from '../internal/request-options'; /** - * Accounts operations + * Manage connected chat accounts */ export class Accounts extends APIResource { /** diff --git a/src/resources/chats/chats.ts b/src/resources/chats/chats.ts index b4bc923..c91dacc 100644 --- a/src/resources/chats/chats.ts +++ b/src/resources/chats/chats.ts @@ -51,6 +51,25 @@ export class Chats extends APIResource { return this._client.get(path`/v1/chats/${chatID}`, { query, ...options }); } + /** + * List all chats sorted by last activity (most recent first). Combines all + * accounts into a single paginated list. + * + * @example + * ```ts + * // Automatically fetches more pages as needed. + * for await (const chatListResponse of client.chats.list()) { + * // ... + * } + * ``` + */ + list( + query: ChatListParams | null | undefined = {}, + options?: RequestOptions, + ): PagePromise { + return this._client.getAPIList('/v1/chats', Cursor, { query, ...options }); + } + /** * Archive or unarchive a chat. Set archived=true to move to archive, * archived=false to move back to inbox @@ -90,6 +109,8 @@ export class Chats extends APIResource { } } +export type ChatListResponsesCursor = Cursor; + export type ChatsCursor = Cursor; export interface Chat { @@ -189,6 +210,13 @@ export interface ChatCreateResponse extends Shared.BaseResponse { chatID?: string; } +export interface ChatListResponse extends Chat { + /** + * Last message preview for this chat, if available. + */ + preview?: Shared.Message; +} + export interface ChatCreateParams { /** * Account to create the chat on. @@ -225,6 +253,13 @@ export interface ChatRetrieveParams { maxParticipantCount?: number | null; } +export interface ChatListParams extends CursorParams { + /** + * Limit to specific account IDs. If omitted, fetches from all accounts. + */ + accountIDs?: Array; +} + export interface ChatArchiveParams { /** * True to archive, false to unarchive @@ -293,9 +328,12 @@ export declare namespace Chats { export { type Chat as Chat, type ChatCreateResponse as ChatCreateResponse, + type ChatListResponse as ChatListResponse, + type ChatListResponsesCursor as ChatListResponsesCursor, type ChatsCursor as ChatsCursor, type ChatCreateParams as ChatCreateParams, type ChatRetrieveParams as ChatRetrieveParams, + type ChatListParams as ChatListParams, type ChatArchiveParams as ChatArchiveParams, type ChatSearchParams as ChatSearchParams, }; diff --git a/src/resources/chats/index.ts b/src/resources/chats/index.ts index 3410cad..16c70b2 100644 --- a/src/resources/chats/index.ts +++ b/src/resources/chats/index.ts @@ -4,10 +4,13 @@ export { Chats, type Chat, type ChatCreateResponse, + type ChatListResponse, type ChatCreateParams, type ChatRetrieveParams, + type ChatListParams, type ChatArchiveParams, type ChatSearchParams, + type ChatListResponsesCursor, type ChatsCursor, } from './chats'; export { Reminders, type ReminderCreateParams } from './reminders'; diff --git a/src/resources/index.ts b/src/resources/index.ts index 71a5a82..c18c512 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -6,16 +6,20 @@ export { Chats, type Chat, type ChatCreateResponse, + type ChatListResponse, type ChatCreateParams, type ChatRetrieveParams, + type ChatListParams, type ChatArchiveParams, type ChatSearchParams, + type ChatListResponsesCursor, type ChatsCursor, } from './chats/chats'; export { Contacts, type ContactSearchResponse, type ContactSearchParams } from './contacts'; export { Messages, type MessageSendResponse, + type MessageListParams, type MessageSearchParams, type MessageSendParams, } from './messages'; diff --git a/src/resources/messages.ts b/src/resources/messages.ts index 8f1d91b..c48b803 100644 --- a/src/resources/messages.ts +++ b/src/resources/messages.ts @@ -11,6 +11,23 @@ import { RequestOptions } from '../internal/request-options'; * Messages operations */ export class Messages extends APIResource { + /** + * List all messages in a chat with cursor-based pagination. Sorted by timestamp. + * + * @example + * ```ts + * // Automatically fetches more pages as needed. + * for await (const message of client.messages.list({ + * chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', + * })) { + * // ... + * } + * ``` + */ + list(query: MessageListParams, options?: RequestOptions): PagePromise { + return this._client.getAPIList('/v1/messages', Cursor, { query, ...options }); + } + /** * Search messages across chats using Beeper's message index * @@ -57,6 +74,13 @@ export interface MessageSendResponse extends Shared.BaseResponse { pendingMessageID: string; } +export interface MessageListParams extends CursorParams { + /** + * The chat ID to list messages from + */ + chatID: string; +} + export interface MessageSearchParams extends CursorParams { /** * Limit search to specific account IDs. @@ -138,6 +162,7 @@ export interface MessageSendParams { export declare namespace Messages { export { type MessageSendResponse as MessageSendResponse, + type MessageListParams as MessageListParams, type MessageSearchParams as MessageSearchParams, type MessageSendParams as MessageSendParams, }; diff --git a/tests/api-resources/chats/chats.test.ts b/tests/api-resources/chats/chats.test.ts index a054ec2..9e1cd5e 100644 --- a/tests/api-resources/chats/chats.test.ts +++ b/tests/api-resources/chats/chats.test.ts @@ -55,6 +55,36 @@ describe('resource chats', () => { ).rejects.toThrow(BeeperDesktop.NotFoundError); }); + test('list', async () => { + const responsePromise = client.chats.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); + }); + + test('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.chats.list( + { + accountIDs: [ + 'whatsapp', + 'local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc', + 'local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU', + ], + cursor: '1725489123456', + direction: 'before', + limit: 1, + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(BeeperDesktop.NotFoundError); + }); + test('archive', async () => { const responsePromise = client.chats.archive('!NCdzlIaMjZUmvmvyHU:beeper.com'); const rawResponse = await responsePromise.asResponse(); diff --git a/tests/api-resources/messages.test.ts b/tests/api-resources/messages.test.ts index 49910d6..33d4c7a 100644 --- a/tests/api-resources/messages.test.ts +++ b/tests/api-resources/messages.test.ts @@ -8,6 +8,26 @@ const client = new BeeperDesktop({ }); describe('resource messages', () => { + test('list: only required params', async () => { + const responsePromise = client.messages.list({ chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com' }); + 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('list: required and optional params', async () => { + const response = await client.messages.list({ + chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', + cursor: '821744079', + direction: 'before', + limit: 1, + }); + }); + test('search', async () => { const responsePromise = client.messages.search(); const rawResponse = await responsePromise.asResponse(); From 80f880478414edcba479e4717c5a1033a2545d3d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:07:36 +0000 Subject: [PATCH 26/36] feat(api): manual updates --- .stats.yml | 2 +- api.md | 12 +--- packages/mcp-server/README.md | 5 +- packages/mcp-server/src/tools/index.ts | 4 +- .../get-token-info-client.ts} | 6 +- src/client.ts | 22 ++++--- src/resources/index.ts | 2 +- src/resources/token.ts | 58 ------------------- src/resources/top-level.ts | 38 ++++++++++++ tests/api-resources/token.test.ts | 21 ------- tests/api-resources/top-level.test.ts | 11 ++++ 11 files changed, 73 insertions(+), 108 deletions(-) rename packages/mcp-server/src/tools/{token/info-token.ts => top-level/get-token-info-client.ts} (87%) delete mode 100644 src/resources/token.ts delete mode 100644 tests/api-resources/token.test.ts diff --git a/.stats.yml b/.stats.yml index e531b74..3cb504d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: be3f3b31e322be0f4de6a23e32ab004c +config_hash: 00db138e547960c0d9c47754c2f59051 diff --git a/api.md b/api.md index 1647294..0841216 100644 --- a/api.md +++ b/api.md @@ -3,12 +3,14 @@ Types: - DownloadAssetResponse +- GetTokenInfoResponse - OpenResponse - SearchResponse Methods: - client.downloadAsset({ ...params }) -> DownloadAssetResponse +- client.getTokenInfo() -> GetTokenInfoResponse - client.open({ ...params }) -> OpenResponse - client.search({ ...params }) -> SearchResponse @@ -78,13 +80,3 @@ Methods: - client.messages.list({ ...params }) -> MessagesCursor - client.messages.search({ ...params }) -> MessagesCursor - client.messages.send({ ...params }) -> MessageSendResponse - -# Token - -Types: - -- UserInfo - -Methods: - -- client.token.info() -> UserInfo diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 1f28201..589fd85 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -215,6 +215,7 @@ The following tools are available in this MCP server. ### Resource `$client`: - `download_asset_client` (`write`): Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL. +- `get_token_info_client` (`read`): Returns information about the authenticated user/token - `open_in_app` (`write`) tags: [app]: Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment. - `search` (`read`) tags: [app]: Search for chats, participant name matches in groups, and the first page of messages in one call. Use this when the user asks for a specific chat, group, or person. @@ -256,7 +257,3 @@ The following tools are available in this MCP server. • "Who are the participants?" (use scope="participants" in search-chats) Returns: matching messages and referenced chats. - `send_message` (`write`) tags: [messages]: 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 `token`: - -- `info_token` (`read`): Returns information about the authenticated user/token diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index f8bc786..f553929 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -5,6 +5,7 @@ import { Metadata, Endpoint, HandlerFunction } from './types'; export { Metadata, Endpoint, HandlerFunction }; import download_asset_client from './top-level/download-asset-client'; +import get_token_info_client from './top-level/get-token-info-client'; import open_in_app from './top-level/open-in-app'; import search from './top-level/search'; import get_accounts from './accounts/get-accounts'; @@ -19,7 +20,6 @@ import clear_chat_reminder from './chats/reminders/clear-chat-reminder'; import list_messages from './messages/list-messages'; import search_messages from './messages/search-messages'; import send_message from './messages/send-message'; -import info_token from './token/info-token'; export const endpoints: Endpoint[] = []; @@ -28,6 +28,7 @@ function addEndpoint(endpoint: Endpoint) { } addEndpoint(download_asset_client); +addEndpoint(get_token_info_client); addEndpoint(open_in_app); addEndpoint(search); addEndpoint(get_accounts); @@ -42,7 +43,6 @@ addEndpoint(clear_chat_reminder); addEndpoint(list_messages); addEndpoint(search_messages); addEndpoint(send_message); -addEndpoint(info_token); export type Filter = { type: 'resource' | 'operation' | 'tag' | 'tool'; diff --git a/packages/mcp-server/src/tools/token/info-token.ts b/packages/mcp-server/src/tools/top-level/get-token-info-client.ts similarity index 87% rename from packages/mcp-server/src/tools/token/info-token.ts rename to packages/mcp-server/src/tools/top-level/get-token-info-client.ts index 2ef864a..d80af75 100644 --- a/packages/mcp-server/src/tools/token/info-token.ts +++ b/packages/mcp-server/src/tools/top-level/get-token-info-client.ts @@ -6,7 +6,7 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; export const metadata: Metadata = { - resource: 'token', + resource: '$client', operation: 'read', tags: [], httpMethod: 'get', @@ -15,7 +15,7 @@ export const metadata: Metadata = { }; export const tool: Tool = { - name: 'info_token', + name: 'get_token_info_client', description: 'Returns information about the authenticated user/token', inputSchema: { type: 'object', @@ -28,7 +28,7 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - return asTextContentResult(await client.token.info()); + return asTextContentResult(await client.getTokenInfo()); }; export default { metadata, tool, handler }; diff --git a/src/client.ts b/src/client.ts index b5049ef..76286ee 100644 --- a/src/client.ts +++ b/src/client.ts @@ -22,6 +22,7 @@ import * as TopLevelAPI from './resources/top-level'; import { DownloadAssetParams, DownloadAssetResponse, + GetTokenInfoResponse, OpenParams, OpenResponse, SearchParams, @@ -37,7 +38,6 @@ import { MessageSendResponse, Messages, } from './resources/messages'; -import { Token, UserInfo } from './resources/token'; import { Chat, ChatArchiveParams, @@ -264,6 +264,18 @@ export class BeeperDesktop { return this.post('/v1/app/download-asset', { body, ...options }); } + /** + * Returns information about the authenticated user/token + * + * @example + * ```ts + * const response = await client.getTokenInfo(); + * ``` + */ + getTokenInfo(options?: RequestOptions): APIPromise { + return this.get('/oauth/userinfo', options); + } + /** * Open Beeper Desktop and optionally navigate to a specific chat, message, or * pre-fill draft text and attachment. @@ -829,17 +841,12 @@ export class BeeperDesktop { * Messages operations */ messages: API.Messages = new API.Messages(this); - /** - * Operations related to the current access token - */ - token: API.Token = new API.Token(this); } BeeperDesktop.Accounts = Accounts; BeeperDesktop.Contacts = Contacts; BeeperDesktop.Chats = Chats; BeeperDesktop.Messages = Messages; -BeeperDesktop.Token = Token; export declare namespace BeeperDesktop { export type RequestOptions = Opts.RequestOptions; @@ -849,6 +856,7 @@ export declare namespace BeeperDesktop { export { type DownloadAssetResponse as DownloadAssetResponse, + type GetTokenInfoResponse as GetTokenInfoResponse, type OpenResponse as OpenResponse, type SearchResponse as SearchResponse, type DownloadAssetParams as DownloadAssetParams, @@ -886,8 +894,6 @@ export declare namespace BeeperDesktop { type MessageSendParams as MessageSendParams, }; - export { Token as Token, type UserInfo as UserInfo }; - export type Attachment = API.Attachment; export type BaseResponse = API.BaseResponse; export type Error = API.Error; diff --git a/src/resources/index.ts b/src/resources/index.ts index c18c512..b668dc5 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -23,9 +23,9 @@ export { type MessageSearchParams, type MessageSendParams, } from './messages'; -export { Token, type UserInfo } from './token'; export { type DownloadAssetResponse, + type GetTokenInfoResponse, type OpenResponse, type SearchResponse, type DownloadAssetParams, diff --git a/src/resources/token.ts b/src/resources/token.ts deleted file mode 100644 index d165da7..0000000 --- a/src/resources/token.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 { APIPromise } from '../core/api-promise'; -import { RequestOptions } from '../internal/request-options'; - -/** - * Operations related to the current access token - */ -export class Token extends APIResource { - /** - * Returns information about the authenticated user/token - */ - info(options?: RequestOptions): APIPromise { - return this._client.get('/oauth/userinfo', options); - } -} - -export interface UserInfo { - /** - * Issued at timestamp (Unix epoch seconds) - */ - iat: number; - - /** - * Granted scopes - */ - scope: string; - - /** - * Subject identifier (token ID) - */ - sub: string; - - /** - * Token type - */ - token_use: 'access'; - - /** - * Audience (client ID) - */ - aud?: string; - - /** - * Client identifier - */ - client_id?: string; - - /** - * Expiration timestamp (Unix epoch seconds) - */ - exp?: number; -} - -export declare namespace Token { - export { type UserInfo as UserInfo }; -} diff --git a/src/resources/top-level.ts b/src/resources/top-level.ts index 7a9eafe..071d63e 100644 --- a/src/resources/top-level.ts +++ b/src/resources/top-level.ts @@ -15,6 +15,43 @@ export interface DownloadAssetResponse { srcURL?: string; } +export interface GetTokenInfoResponse { + /** + * Issued at timestamp (Unix epoch seconds) + */ + iat: number; + + /** + * Granted scopes + */ + scope: string; + + /** + * Subject identifier (token ID) + */ + sub: string; + + /** + * Token type + */ + token_use: 'access'; + + /** + * Audience (client ID) + */ + aud?: string; + + /** + * Client identifier + */ + client_id?: string; + + /** + * Expiration timestamp (Unix epoch seconds) + */ + exp?: number; +} + /** * Response indicating successful app opening. */ @@ -116,6 +153,7 @@ export interface SearchParams { export declare namespace TopLevel { export { type DownloadAssetResponse as DownloadAssetResponse, + type GetTokenInfoResponse as GetTokenInfoResponse, type OpenResponse as OpenResponse, type SearchResponse as SearchResponse, type DownloadAssetParams as DownloadAssetParams, diff --git a/tests/api-resources/token.test.ts b/tests/api-resources/token.test.ts deleted file mode 100644 index 55e8716..0000000 --- a/tests/api-resources/token.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import BeeperDesktop from '@beeper/desktop-api'; - -const client = new BeeperDesktop({ - accessToken: 'My Access Token', - baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', -}); - -describe('resource token', () => { - test('info', async () => { - const responsePromise = client.token.info(); - 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); - }); -}); diff --git a/tests/api-resources/top-level.test.ts b/tests/api-resources/top-level.test.ts index 9ad90a9..4e1a94e 100644 --- a/tests/api-resources/top-level.test.ts +++ b/tests/api-resources/top-level.test.ts @@ -23,6 +23,17 @@ describe('top level methods', () => { const response = await client.downloadAsset({ url: 'mxc://example.org/Q4x9CqGz1pB3Oa6XgJ' }); }); + test('getTokenInfo', async () => { + const responsePromise = client.getTokenInfo(); + 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('open', async () => { const responsePromise = client.open(); const rawResponse = await responsePromise.asResponse(); From d7c35d27538be9752a6e15a4d17c4b5354b44b3f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:13:38 +0000 Subject: [PATCH 27/36] feat(api): manual updates --- .stats.yml | 2 +- packages/mcp-server/README.md | 13 +--- .../src/tools/chats/create-chats.ts | 60 ------------------ .../mcp-server/src/tools/chats/list-chats.ts | 61 ------------------- .../src/tools/contacts/search-contacts.ts | 45 -------------- packages/mcp-server/src/tools/index.ts | 12 ---- .../src/tools/messages/list-messages.ts | 55 ----------------- .../tools/top-level/download-asset-client.ts | 38 ------------ .../tools/top-level/get-token-info-client.ts | 34 ----------- 9 files changed, 3 insertions(+), 317 deletions(-) delete mode 100644 packages/mcp-server/src/tools/chats/create-chats.ts delete mode 100644 packages/mcp-server/src/tools/chats/list-chats.ts delete mode 100644 packages/mcp-server/src/tools/contacts/search-contacts.ts delete mode 100644 packages/mcp-server/src/tools/messages/list-messages.ts delete mode 100644 packages/mcp-server/src/tools/top-level/download-asset-client.ts delete mode 100644 packages/mcp-server/src/tools/top-level/get-token-info-client.ts diff --git a/.stats.yml b/.stats.yml index 3cb504d..dffcc04 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: 00db138e547960c0d9c47754c2f59051 +config_hash: 382b53633aa9cc48d7b7f44ecf5e3e8c diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 589fd85..cb138cb 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -180,7 +180,7 @@ http://localhost:3000?client=cursor&capability=tool-name-length%3D40 import { server, endpoints, init } from "@beeper/desktop-api-mcp/server"; // import a specific tool -import downloadAssetClient from "@beeper/desktop-api-mcp/tools/top-level/download-asset-client"; +import openInApp from "@beeper/desktop-api-mcp/tools/top-level/open-in-app"; // initialize the server and all endpoints init({ server, endpoints }); @@ -205,7 +205,7 @@ const myCustomEndpoint = { }; // initialize the server with your custom endpoints -init({ server: myServer, endpoints: [downloadAssetClient, myCustomEndpoint] }); +init({ server: myServer, endpoints: [openInApp, myCustomEndpoint] }); ``` ## Available Tools @@ -214,8 +214,6 @@ The following tools are available in this MCP server. ### Resource `$client`: -- `download_asset_client` (`write`): Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL. -- `get_token_info_client` (`read`): Returns information about the authenticated user/token - `open_in_app` (`write`) tags: [app]: Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment. - `search` (`read`) tags: [app]: Search for chats, participant name matches in groups, and the first page of messages in one call. Use this when the user asks for a specific chat, group, or person. @@ -223,15 +221,9 @@ The following tools are available in this MCP server. - `get_accounts` (`read`) tags: [accounts]: List connected accounts on this device. -### Resource `contacts`: - -- `search_contacts` (`read`): Search contacts across on a specific account using the network's search API. Only use for creating new chats. - ### Resource `chats`: -- `create_chats` (`write`): Create a single or group chat on a specific account using participant IDs and optional title. - `get_chat` (`read`) tags: [chats]: Get chat details: metadata, participants (limited), last activity. -- `list_chats` (`read`): List all chats sorted by last activity (most recent first). Combines all accounts into a single paginated list. - `archive_chat` (`write`) tags: [chats]: Archive or unarchive a chat. - `search_chats` (`read`) tags: [chats]: Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'. @@ -242,7 +234,6 @@ The following tools are available in this MCP server. ### Resource `messages`: -- `list_messages` (`read`): List all messages in a chat with cursor-based pagination. Sorted by timestamp. - `search_messages` (`read`) tags: [messages]: 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. diff --git a/packages/mcp-server/src/tools/chats/create-chats.ts b/packages/mcp-server/src/tools/chats/create-chats.ts deleted file mode 100644 index 23ee694..0000000 --- a/packages/mcp-server/src/tools/chats/create-chats.ts +++ /dev/null @@ -1,60 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'chats', - operation: 'write', - tags: [], - httpMethod: 'post', - httpPath: '/v1/chats', - operationId: 'createChat', -}; - -export const tool: Tool = { - name: 'create_chats', - description: - 'Create a single or group chat on a specific account using participant IDs and optional title.', - inputSchema: { - type: 'object', - properties: { - accountID: { - type: 'string', - description: 'Account to create the chat on.', - }, - participantIDs: { - type: 'array', - description: 'User IDs to include in the new chat.', - items: { - type: 'string', - }, - }, - type: { - type: 'string', - description: - "Chat type to create: 'single' requires exactly one participantID; 'group' supports multiple participants and optional title.", - enum: ['single', 'group'], - }, - messageText: { - type: 'string', - description: 'Optional first message content if the platform requires it to create the chat.', - }, - title: { - type: 'string', - description: 'Optional title for group chats; ignored for single chats on most platforms.', - }, - }, - required: ['accountID', 'participantIDs', 'type'], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.chats.create(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/list-chats.ts b/packages/mcp-server/src/tools/chats/list-chats.ts deleted file mode 100644 index 5013aa0..0000000 --- a/packages/mcp-server/src/tools/chats/list-chats.ts +++ /dev/null @@ -1,61 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'chats', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v1/chats', - operationId: 'listChats', -}; - -export const tool: Tool = { - name: 'list_chats', - description: - 'List all chats sorted by last activity (most recent first). Combines all accounts into a single paginated list.', - inputSchema: { - type: 'object', - properties: { - accountIDs: { - type: 'array', - description: 'Limit to specific account IDs. If omitted, fetches from all accounts.', - items: { - type: 'string', - description: 'Account ID this resource belongs to.', - }, - }, - cursor: { - type: 'string', - description: - 'Timestamp cursor (milliseconds since epoch) for pagination. Use with direction to navigate results.', - }, - direction: { - type: 'string', - description: - "Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided.", - enum: ['after', 'before'], - }, - limit: { - type: 'integer', - description: 'Maximum number of chats to return (1–200). Defaults to 50.', - }, - }, - required: [], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - const response = await client.chats.list(body).asResponse(); - return asTextContentResult(await response.json()); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/contacts/search-contacts.ts b/packages/mcp-server/src/tools/contacts/search-contacts.ts deleted file mode 100644 index b889569..0000000 --- a/packages/mcp-server/src/tools/contacts/search-contacts.ts +++ /dev/null @@ -1,45 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'contacts', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v1/contacts/search', - operationId: 'searchContacts', -}; - -export const tool: Tool = { - name: 'search_contacts', - description: - "Search contacts across on a specific account using the network's search API. Only use for creating new chats.", - inputSchema: { - type: 'object', - properties: { - accountID: { - type: 'string', - description: 'Account ID this resource belongs to.', - }, - query: { - type: 'string', - description: 'Text to search users by. Network-specific behavior.', - }, - }, - required: ['accountID', 'query'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.contacts.search(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts index f553929..4f1144e 100644 --- a/packages/mcp-server/src/tools/index.ts +++ b/packages/mcp-server/src/tools/index.ts @@ -4,20 +4,14 @@ import { Metadata, Endpoint, HandlerFunction } from './types'; export { Metadata, Endpoint, HandlerFunction }; -import download_asset_client from './top-level/download-asset-client'; -import get_token_info_client from './top-level/get-token-info-client'; import open_in_app from './top-level/open-in-app'; import search from './top-level/search'; import get_accounts from './accounts/get-accounts'; -import search_contacts from './contacts/search-contacts'; -import create_chats from './chats/create-chats'; import get_chat from './chats/get-chat'; -import list_chats from './chats/list-chats'; import archive_chat from './chats/archive-chat'; import search_chats from './chats/search-chats'; import set_chat_reminder from './chats/reminders/set-chat-reminder'; import clear_chat_reminder from './chats/reminders/clear-chat-reminder'; -import list_messages from './messages/list-messages'; import search_messages from './messages/search-messages'; import send_message from './messages/send-message'; @@ -27,20 +21,14 @@ function addEndpoint(endpoint: Endpoint) { endpoints.push(endpoint); } -addEndpoint(download_asset_client); -addEndpoint(get_token_info_client); addEndpoint(open_in_app); addEndpoint(search); addEndpoint(get_accounts); -addEndpoint(search_contacts); -addEndpoint(create_chats); addEndpoint(get_chat); -addEndpoint(list_chats); addEndpoint(archive_chat); addEndpoint(search_chats); addEndpoint(set_chat_reminder); addEndpoint(clear_chat_reminder); -addEndpoint(list_messages); addEndpoint(search_messages); addEndpoint(send_message); diff --git a/packages/mcp-server/src/tools/messages/list-messages.ts b/packages/mcp-server/src/tools/messages/list-messages.ts deleted file mode 100644 index 91d7c62..0000000 --- a/packages/mcp-server/src/tools/messages/list-messages.ts +++ /dev/null @@ -1,55 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: 'messages', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v1/messages', - operationId: 'listMessages', -}; - -export const tool: Tool = { - name: 'list_messages', - description: 'List all messages in a chat with cursor-based pagination. Sorted by timestamp.', - inputSchema: { - type: 'object', - properties: { - chatID: { - type: 'string', - description: 'The chat ID to list messages from', - }, - cursor: { - type: 'string', - description: 'Message cursor for pagination. Use with direction to navigate results.', - }, - direction: { - type: 'string', - description: - "Pagination direction used with 'cursor': 'before' fetches older messages, 'after' fetches newer messages. Defaults to 'before' when only 'cursor' is provided.", - enum: ['after', 'before'], - }, - limit: { - type: 'integer', - description: 'Maximum number of messages to return (1–500). Defaults to 50.', - }, - }, - required: ['chatID'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - const response = await client.messages.list(body).asResponse(); - return asTextContentResult(await response.json()); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/download-asset-client.ts b/packages/mcp-server/src/tools/top-level/download-asset-client.ts deleted file mode 100644 index ed39eb3..0000000 --- a/packages/mcp-server/src/tools/top-level/download-asset-client.ts +++ /dev/null @@ -1,38 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: '$client', - operation: 'write', - tags: [], - httpMethod: 'post', - httpPath: '/v1/app/download-asset', - operationId: 'downloadAsset', -}; - -export const tool: Tool = { - name: 'download_asset_client', - description: 'Download a Matrix asset using its mxc:// or localmxc:// URL and return the local file URL.', - inputSchema: { - type: 'object', - properties: { - url: { - type: 'string', - description: 'Matrix content URL (mxc:// or localmxc://) for the asset to download.', - }, - }, - required: ['url'], - }, - annotations: {}, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.downloadAsset(body)); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/get-token-info-client.ts b/packages/mcp-server/src/tools/top-level/get-token-info-client.ts deleted file mode 100644 index d80af75..0000000 --- a/packages/mcp-server/src/tools/top-level/get-token-info-client.ts +++ /dev/null @@ -1,34 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import BeeperDesktop from '@beeper/desktop-api'; - -export const metadata: Metadata = { - resource: '$client', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/oauth/userinfo', - operationId: 'oauth_get_user_info', -}; - -export const tool: Tool = { - name: 'get_token_info_client', - description: 'Returns information about the authenticated user/token', - inputSchema: { - type: 'object', - properties: {}, - required: [], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - return asTextContentResult(await client.getTokenInfo()); -}; - -export default { metadata, tool, handler }; From 0de85a39f784415474e73187609e4ef4c5fe2cf1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:15:32 +0000 Subject: [PATCH 28/36] feat(api): manual updates --- .stats.yml | 2 +- .../src/tools/accounts/get-accounts.ts | 16 +++++++++++++--- .../mcp-server/src/tools/chats/archive-chat.ts | 14 +++++++++++--- packages/mcp-server/src/tools/chats/get-chat.ts | 14 +++++++++++--- .../tools/chats/reminders/clear-chat-reminder.ts | 14 +++++++++++--- .../tools/chats/reminders/set-chat-reminder.ts | 14 +++++++++++--- .../mcp-server/src/tools/chats/search-chats.ts | 13 ++++++++++--- .../src/tools/messages/send-message.ts | 13 ++++++++++--- .../src/tools/top-level/open-in-app.ts | 13 ++++++++++--- 9 files changed, 88 insertions(+), 25 deletions(-) diff --git a/.stats.yml b/.stats.yml index dffcc04..b4e4371 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: 382b53633aa9cc48d7b7f44ecf5e3e8c +config_hash: 58f19d979ad9a375e32b814503ce3e86 diff --git a/packages/mcp-server/src/tools/accounts/get-accounts.ts b/packages/mcp-server/src/tools/accounts/get-accounts.ts index ad88134..1bb3455 100644 --- a/packages/mcp-server/src/tools/accounts/get-accounts.ts +++ b/packages/mcp-server/src/tools/accounts/get-accounts.ts @@ -1,5 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -16,10 +17,18 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'get_accounts', - description: 'List connected accounts on this device.', + 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 accounts on this device.\n\n# Response Schema\n```json\n{\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 $defs: {\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\\').'\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: 'User the account belongs to.',\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: {}, + 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: { @@ -28,7 +37,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - return asTextContentResult(await client.accounts.list()); + 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/chats/archive-chat.ts b/packages/mcp-server/src/tools/chats/archive-chat.ts index 7094bda..19efcf0 100644 --- a/packages/mcp-server/src/tools/chats/archive-chat.ts +++ b/packages/mcp-server/src/tools/chats/archive-chat.ts @@ -1,5 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -16,7 +17,8 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'archive_chat', - description: 'Archive or unarchive a chat.', + 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.\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: { @@ -29,6 +31,12 @@ export const tool: Tool = { 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'], }, @@ -36,8 +44,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { chatID, ...body } = args as any; - return asTextContentResult(await client.chats.archive(chatID, body)); + const { chatID, jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.chats.archive(chatID, body))); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/get-chat.ts b/packages/mcp-server/src/tools/chats/get-chat.ts index 02f13bf..7a4c7d2 100644 --- a/packages/mcp-server/src/tools/chats/get-chat.ts +++ b/packages/mcp-server/src/tools/chats/get-chat.ts @@ -1,5 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -16,7 +17,8 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'get_chat', - description: 'Get chat details: metadata, participants (limited), last activity.', + 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\nGet chat details: metadata, participants (limited), last activity.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/chat',\n $defs: {\n chat: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier of the chat (room/thread ID, same as id) across Beeper.'\n },\n accountID: {\n type: 'string',\n description: 'Beeper account ID this chat belongs to.'\n },\n network: {\n type: 'string',\n description: 'Display-only human-readable network name (e.g., \\'WhatsApp\\', \\'Messenger\\').'\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.',\n enum: [ 'single',\n 'group'\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 localChatID: {\n type: 'string',\n description: 'Local chat ID specific to this Beeper Desktop installation.'\n }\n },\n required: [ 'id',\n 'accountID',\n 'network',\n 'participants',\n 'title',\n 'type',\n 'unreadCount'\n ]\n },\n user: {\n type: 'object',\n description: 'User the account belongs to.',\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: { @@ -30,6 +32,12 @@ export const tool: Tool = { 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'], }, @@ -39,8 +47,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { chatID, ...body } = args as any; - return asTextContentResult(await client.chats.retrieve(chatID, body)); + const { chatID, jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.chats.retrieve(chatID, body))); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts index 7e07b5c..5982e99 100644 --- a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts +++ b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts @@ -1,5 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -16,7 +17,8 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'clear_chat_reminder', - description: 'Clear a chat reminder.', + 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 a chat reminder.\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: { @@ -25,6 +27,12 @@ export const tool: Tool = { description: 'The identifier of the chat to clear reminder from (accepts both chatID and local chat ID)', }, + 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'], }, @@ -34,8 +42,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { chatID, ...body } = args as any; - return asTextContentResult(await client.chats.reminders.delete(chatID)); + const { chatID, jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.chats.reminders.delete(chatID))); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts index 4c3473a..c2a26ca 100644 --- a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts +++ b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts @@ -1,5 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -16,7 +17,8 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'set_chat_reminder', - description: 'Set a reminder for a chat at a specific time.', + 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: { @@ -39,6 +41,12 @@ export const tool: Tool = { }, 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'], }, @@ -46,8 +54,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { chatID, ...body } = args as any; - return asTextContentResult(await client.chats.reminders.create(chatID, body)); + const { chatID, jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.chats.reminders.create(chatID, body))); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/search-chats.ts b/packages/mcp-server/src/tools/chats/search-chats.ts index 37dabd9..d652125 100644 --- a/packages/mcp-server/src/tools/chats/search-chats.ts +++ b/packages/mcp-server/src/tools/chats/search-chats.ts @@ -1,5 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -17,7 +18,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'search_chats', description: - "Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'.", + "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 chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'.\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n hasMore: {\n type: 'boolean',\n description: 'True if additional results can be fetched using the provided cursors.'\n },\n items: {\n type: 'array',\n description: 'Chats matching the filters.',\n items: {\n $ref: '#/$defs/chat'\n }\n },\n newestCursor: {\n type: 'string',\n description: 'Cursor for fetching newer results (use with direction=\\'after\\'). Opaque string; do not inspect.'\n },\n oldestCursor: {\n type: 'string',\n description: 'Cursor for fetching older results (use with direction=\\'before\\'). Opaque string; do not inspect.'\n }\n },\n required: [ 'hasMore',\n 'items',\n 'newestCursor',\n 'oldestCursor'\n ],\n $defs: {\n chat: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier of the chat (room/thread ID, same as id) across Beeper.'\n },\n accountID: {\n type: 'string',\n description: 'Beeper account ID this chat belongs to.'\n },\n network: {\n type: 'string',\n description: 'Display-only human-readable network name (e.g., \\'WhatsApp\\', \\'Messenger\\').'\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.',\n enum: [ 'single',\n 'group'\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 localChatID: {\n type: 'string',\n description: 'Local chat ID specific to this Beeper Desktop installation.'\n }\n },\n required: [ 'id',\n 'accountID',\n 'network',\n 'participants',\n 'title',\n 'type',\n 'unreadCount'\n ]\n },\n user: {\n type: 'object',\n description: 'User the account belongs to.',\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: { @@ -87,6 +88,12 @@ export const tool: Tool = { 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: [], }, @@ -96,9 +103,9 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; + const { jq_filter, ...body } = args as any; const response = await client.chats.search(body).asResponse(); - return asTextContentResult(await response.json()); + return asTextContentResult(await maybeFilter(jq_filter, await response.json())); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/messages/send-message.ts b/packages/mcp-server/src/tools/messages/send-message.ts index 81d7ba4..620cb9a 100644 --- a/packages/mcp-server/src/tools/messages/send-message.ts +++ b/packages/mcp-server/src/tools/messages/send-message.ts @@ -1,5 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -17,7 +18,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'send_message', description: - 'Send a text message to a specific chat. Supports replying to existing messages. Returns the sent message ID and a deeplink to the chat', + "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 allOf: [ {\n $ref: '#/$defs/base_response'\n }\n ],\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: { @@ -33,6 +34,12 @@ export const tool: Tool = { 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'], }, @@ -40,8 +47,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.messages.send(body)); + 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/top-level/open-in-app.ts b/packages/mcp-server/src/tools/top-level/open-in-app.ts index 3743bfe..67be8d5 100644 --- a/packages/mcp-server/src/tools/top-level/open-in-app.ts +++ b/packages/mcp-server/src/tools/top-level/open-in-app.ts @@ -1,5 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -17,7 +18,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'open_in_app', description: - 'Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment.', + "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\nOpen Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment.\n\n# Response Schema\n```json\n{\n type: 'object',\n description: 'Response indicating successful app opening.',\n properties: {\n success: {\n type: 'boolean',\n description: 'Whether the app was successfully opened/focused.'\n }\n },\n required: [ 'success'\n ]\n}\n```", inputSchema: { type: 'object', properties: { @@ -38,6 +39,12 @@ export const tool: Tool = { type: 'string', description: 'Optional message ID. Jumps to that message in the chat when opening.', }, + 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: [], }, @@ -45,8 +52,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const body = args as any; - return asTextContentResult(await client.open(body)); + const { jq_filter, ...body } = args as any; + return asTextContentResult(await maybeFilter(jq_filter, await client.open(body))); }; export default { metadata, tool, handler }; From e657efe6bf48b6c76e09060ac00c8e0403213982 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:17:42 +0000 Subject: [PATCH 29/36] feat(api): manual updates --- .stats.yml | 2 +- packages/mcp-server/README.md | 8 ++++---- packages/mcp-server/build | 2 +- packages/mcp-server/jest.config.ts | 4 ++-- packages/mcp-server/manifest.json | 2 +- packages/mcp-server/package.json | 6 +++--- packages/mcp-server/src/tools/accounts/get-accounts.ts | 4 ++-- packages/mcp-server/src/tools/chats/archive-chat.ts | 4 ++-- packages/mcp-server/src/tools/chats/get-chat.ts | 4 ++-- .../src/tools/chats/reminders/clear-chat-reminder.ts | 4 ++-- .../src/tools/chats/reminders/set-chat-reminder.ts | 4 ++-- packages/mcp-server/src/tools/chats/search-chats.ts | 4 ++-- packages/mcp-server/src/tools/messages/search-messages.ts | 2 +- packages/mcp-server/src/tools/messages/send-message.ts | 4 ++-- packages/mcp-server/src/tools/top-level/open-in-app.ts | 4 ++-- packages/mcp-server/src/tools/top-level/search.ts | 2 +- packages/mcp-server/tsconfig.build.json | 4 ++-- packages/mcp-server/tsconfig.json | 4 ++-- 18 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.stats.yml b/.stats.yml index b4e4371..5b8f55b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: 58f19d979ad9a375e32b814503ce3e86 +config_hash: a9434fa7b77fc01af6e667f3717eb768 diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index cb138cb..2616e14 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -17,7 +17,7 @@ You can run the MCP Server directly via `npx`: ```sh export BEEPER_ACCESS_TOKEN="My Access Token" -npx -y @beeper/desktop-api-mcp@latest +npx -y @beeper/desktop-mcp@latest ``` ### Via MCP Client @@ -32,7 +32,7 @@ For clients with a configuration JSON, it might look something like this: "mcpServers": { "beeper_desktop_api_api": { "command": "npx", - "args": ["-y", "@beeper/desktop-api-mcp", "--client=claude", "--tools=all"], + "args": ["-y", "@beeper/desktop-mcp", "--client=claude", "--tools=all"], "env": { "BEEPER_ACCESS_TOKEN": "My Access Token" } @@ -177,10 +177,10 @@ http://localhost:3000?client=cursor&capability=tool-name-length%3D40 ```js // Import the server, generated endpoints, or the init function -import { server, endpoints, init } from "@beeper/desktop-api-mcp/server"; +import { server, endpoints, init } from "@beeper/desktop-mcp/server"; // import a specific tool -import openInApp from "@beeper/desktop-api-mcp/tools/top-level/open-in-app"; +import openInApp from "@beeper/desktop-mcp/tools/top-level/open-in-app"; // initialize the server and all endpoints init({ server, endpoints }); diff --git a/packages/mcp-server/build b/packages/mcp-server/build index c8808cc..b94538a 100644 --- a/packages/mcp-server/build +++ b/packages/mcp-server/build @@ -29,7 +29,7 @@ cp tsconfig.dist-src.json dist/src/tsconfig.json chmod +x dist/index.js -DIST_PATH=./dist PKG_IMPORT_PATH=@beeper/desktop-api-mcp/ node ../../scripts/utils/postprocess-files.cjs +DIST_PATH=./dist PKG_IMPORT_PATH=@beeper/desktop-mcp/ node ../../scripts/utils/postprocess-files.cjs # mcp bundle rm -rf dist-bundle beeper_desktop_api_api.mcpb; mkdir dist-bundle diff --git a/packages/mcp-server/jest.config.ts b/packages/mcp-server/jest.config.ts index f660356..5e54047 100644 --- a/packages/mcp-server/jest.config.ts +++ b/packages/mcp-server/jest.config.ts @@ -7,8 +7,8 @@ const config: JestConfigWithTsJest = { '^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }], }, moduleNameMapper: { - '^@beeper/desktop-api-mcp$': '/src/index.ts', - '^@beeper/desktop-api-mcp/(.*)$': '/src/$1', + '^@beeper/desktop-mcp$': '/src/index.ts', + '^@beeper/desktop-mcp/(.*)$': '/src/$1', }, modulePathIgnorePatterns: ['/dist/'], testPathIgnorePatterns: ['scripts'], diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 212d9d1..09047c8 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,6 +1,6 @@ { "dxt_version": "0.2", - "name": "@beeper/desktop-api-mcp", + "name": "@beeper/desktop-mcp", "version": "0.1.4", "description": "The official MCP Server for the Beeper Desktop API", "author": { diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 9234403..b9510db 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,5 +1,5 @@ { - "name": "@beeper/desktop-api-mcp", + "name": "@beeper/desktop-mcp", "version": "0.1.5", "description": "The official MCP Server for the Beeper Desktop API", "author": "Beeper Desktop ", @@ -68,8 +68,8 @@ "typescript": "5.8.3" }, "imports": { - "@beeper/desktop-api-mcp": ".", - "@beeper/desktop-api-mcp/*": "./src/*" + "@beeper/desktop-mcp": ".", + "@beeper/desktop-mcp/*": "./src/*" }, "exports": { ".": { diff --git a/packages/mcp-server/src/tools/accounts/get-accounts.ts b/packages/mcp-server/src/tools/accounts/get-accounts.ts index 1bb3455..dbc32c0 100644 --- a/packages/mcp-server/src/tools/accounts/get-accounts.ts +++ b/packages/mcp-server/src/tools/accounts/get-accounts.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { maybeFilter } from '@beeper/desktop-mcp/filtering'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/archive-chat.ts b/packages/mcp-server/src/tools/chats/archive-chat.ts index 19efcf0..9ec7869 100644 --- a/packages/mcp-server/src/tools/chats/archive-chat.ts +++ b/packages/mcp-server/src/tools/chats/archive-chat.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { maybeFilter } from '@beeper/desktop-mcp/filtering'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/get-chat.ts b/packages/mcp-server/src/tools/chats/get-chat.ts index 7a4c7d2..2e21624 100644 --- a/packages/mcp-server/src/tools/chats/get-chat.ts +++ b/packages/mcp-server/src/tools/chats/get-chat.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { maybeFilter } from '@beeper/desktop-mcp/filtering'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts index 5982e99..4a96e0c 100644 --- a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts +++ b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { maybeFilter } from '@beeper/desktop-mcp/filtering'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts index c2a26ca..41d8510 100644 --- a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts +++ b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { maybeFilter } from '@beeper/desktop-mcp/filtering'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/chats/search-chats.ts b/packages/mcp-server/src/tools/chats/search-chats.ts index d652125..394192e 100644 --- a/packages/mcp-server/src/tools/chats/search-chats.ts +++ b/packages/mcp-server/src/tools/chats/search-chats.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { maybeFilter } from '@beeper/desktop-mcp/filtering'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/messages/search-messages.ts b/packages/mcp-server/src/tools/messages/search-messages.ts index bbf1ea9..93d735c 100644 --- a/packages/mcp-server/src/tools/messages/search-messages.ts +++ b/packages/mcp-server/src/tools/messages/search-messages.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/messages/send-message.ts b/packages/mcp-server/src/tools/messages/send-message.ts index 620cb9a..ee26fae 100644 --- a/packages/mcp-server/src/tools/messages/send-message.ts +++ b/packages/mcp-server/src/tools/messages/send-message.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { maybeFilter } from '@beeper/desktop-mcp/filtering'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/top-level/open-in-app.ts b/packages/mcp-server/src/tools/top-level/open-in-app.ts index 67be8d5..da3560d 100644 --- a/packages/mcp-server/src/tools/top-level/open-in-app.ts +++ b/packages/mcp-server/src/tools/top-level/open-in-app.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-api-mcp/filtering'; -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { maybeFilter } from '@beeper/desktop-mcp/filtering'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/src/tools/top-level/search.ts b/packages/mcp-server/src/tools/top-level/search.ts index aaa483c..ce1bdfb 100644 --- a/packages/mcp-server/src/tools/top-level/search.ts +++ b/packages/mcp-server/src/tools/top-level/search.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from '@beeper/desktop-api-mcp/tools/types'; +import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import BeeperDesktop from '@beeper/desktop-api'; diff --git a/packages/mcp-server/tsconfig.build.json b/packages/mcp-server/tsconfig.build.json index a346070..c1fe977 100644 --- a/packages/mcp-server/tsconfig.build.json +++ b/packages/mcp-server/tsconfig.build.json @@ -5,8 +5,8 @@ "compilerOptions": { "rootDir": "./dist/src", "paths": { - "@beeper/desktop-api-mcp/*": ["./dist/src/*"], - "@beeper/desktop-api-mcp": ["./dist/src/index.ts"] + "@beeper/desktop-mcp/*": ["./dist/src/*"], + "@beeper/desktop-mcp": ["./dist/src/index.ts"] }, "noEmit": false, "declaration": true, diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index d7639fe..c70b6cc 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -8,8 +8,8 @@ "moduleResolution": "node", "esModuleInterop": true, "paths": { - "@beeper/desktop-api-mcp/*": ["./src/*"], - "@beeper/desktop-api-mcp": ["./src/index.ts"] + "@beeper/desktop-mcp/*": ["./src/*"], + "@beeper/desktop-mcp": ["./src/index.ts"] }, "noEmit": true, From 914badc5d98d1b3ab5b7d1b404c687110ef34913 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:22:39 +0000 Subject: [PATCH 30/36] feat(api): manual updates --- .stats.yml | 8 ++-- api.md | 6 +-- .../src/tools/top-level/open-in-app.ts | 2 +- src/client.ts | 18 +-------- src/resources/index.ts | 1 - src/resources/top-level.ts | 38 ------------------- tests/api-resources/top-level.test.ts | 11 ------ 7 files changed, 9 insertions(+), 75 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5b8f55b..d64bab5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-9f5190d7df873112f3512b5796cd95341f0fa0d2585488d3e829be80ee6045ce.yml -openapi_spec_hash: ba834200758376aaea47b2a276f64c1b -config_hash: a9434fa7b77fc01af6e667f3717eb768 +configured_endpoints: 15 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml +openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef +config_hash: d48fc12c89d2d812adf19d0508306f4a diff --git a/api.md b/api.md index 0841216..19e98b5 100644 --- a/api.md +++ b/api.md @@ -3,15 +3,13 @@ Types: - DownloadAssetResponse -- GetTokenInfoResponse - OpenResponse - SearchResponse Methods: -- client.downloadAsset({ ...params }) -> DownloadAssetResponse -- client.getTokenInfo() -> GetTokenInfoResponse -- client.open({ ...params }) -> OpenResponse +- client.downloadAsset({ ...params }) -> DownloadAssetResponse +- client.open({ ...params }) -> OpenResponse - client.search({ ...params }) -> SearchResponse # Shared diff --git a/packages/mcp-server/src/tools/top-level/open-in-app.ts b/packages/mcp-server/src/tools/top-level/open-in-app.ts index da3560d..aa49f0d 100644 --- a/packages/mcp-server/src/tools/top-level/open-in-app.ts +++ b/packages/mcp-server/src/tools/top-level/open-in-app.ts @@ -11,7 +11,7 @@ export const metadata: Metadata = { operation: 'write', tags: ['app'], httpMethod: 'post', - httpPath: '/v1/app/open', + httpPath: '/v1/open', operationId: 'openApp', }; diff --git a/src/client.ts b/src/client.ts index 76286ee..205ef99 100644 --- a/src/client.ts +++ b/src/client.ts @@ -22,7 +22,6 @@ import * as TopLevelAPI from './resources/top-level'; import { DownloadAssetParams, DownloadAssetResponse, - GetTokenInfoResponse, OpenParams, OpenResponse, SearchParams, @@ -261,19 +260,7 @@ export class BeeperDesktop { body: TopLevelAPI.DownloadAssetParams, options?: RequestOptions, ): APIPromise { - return this.post('/v1/app/download-asset', { body, ...options }); - } - - /** - * Returns information about the authenticated user/token - * - * @example - * ```ts - * const response = await client.getTokenInfo(); - * ``` - */ - getTokenInfo(options?: RequestOptions): APIPromise { - return this.get('/oauth/userinfo', options); + return this.post('/v1/download-asset', { body, ...options }); } /** @@ -289,7 +276,7 @@ export class BeeperDesktop { body: TopLevelAPI.OpenParams | null | undefined = {}, options?: RequestOptions, ): APIPromise { - return this.post('/v1/app/open', { body, ...options }); + return this.post('/v1/open', { body, ...options }); } /** @@ -856,7 +843,6 @@ export declare namespace BeeperDesktop { export { type DownloadAssetResponse as DownloadAssetResponse, - type GetTokenInfoResponse as GetTokenInfoResponse, type OpenResponse as OpenResponse, type SearchResponse as SearchResponse, type DownloadAssetParams as DownloadAssetParams, diff --git a/src/resources/index.ts b/src/resources/index.ts index b668dc5..264800e 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -25,7 +25,6 @@ export { } from './messages'; export { type DownloadAssetResponse, - type GetTokenInfoResponse, type OpenResponse, type SearchResponse, type DownloadAssetParams, diff --git a/src/resources/top-level.ts b/src/resources/top-level.ts index 071d63e..7a9eafe 100644 --- a/src/resources/top-level.ts +++ b/src/resources/top-level.ts @@ -15,43 +15,6 @@ export interface DownloadAssetResponse { srcURL?: string; } -export interface GetTokenInfoResponse { - /** - * Issued at timestamp (Unix epoch seconds) - */ - iat: number; - - /** - * Granted scopes - */ - scope: string; - - /** - * Subject identifier (token ID) - */ - sub: string; - - /** - * Token type - */ - token_use: 'access'; - - /** - * Audience (client ID) - */ - aud?: string; - - /** - * Client identifier - */ - client_id?: string; - - /** - * Expiration timestamp (Unix epoch seconds) - */ - exp?: number; -} - /** * Response indicating successful app opening. */ @@ -153,7 +116,6 @@ export interface SearchParams { export declare namespace TopLevel { export { type DownloadAssetResponse as DownloadAssetResponse, - type GetTokenInfoResponse as GetTokenInfoResponse, type OpenResponse as OpenResponse, type SearchResponse as SearchResponse, type DownloadAssetParams as DownloadAssetParams, diff --git a/tests/api-resources/top-level.test.ts b/tests/api-resources/top-level.test.ts index 4e1a94e..9ad90a9 100644 --- a/tests/api-resources/top-level.test.ts +++ b/tests/api-resources/top-level.test.ts @@ -23,17 +23,6 @@ describe('top level methods', () => { const response = await client.downloadAsset({ url: 'mxc://example.org/Q4x9CqGz1pB3Oa6XgJ' }); }); - test('getTokenInfo', async () => { - const responsePromise = client.getTokenInfo(); - 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('open', async () => { const responsePromise = client.open(); const rawResponse = await responsePromise.asResponse(); From e12ab16a4664ef6b770750225dc0e9ea53005b89 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:27:48 +0000 Subject: [PATCH 31/36] feat(api): manual updates --- .stats.yml | 2 +- .../src/tools/accounts/get-accounts.ts | 16 +++------------- .../mcp-server/src/tools/chats/archive-chat.ts | 14 +++----------- packages/mcp-server/src/tools/chats/get-chat.ts | 14 +++----------- .../tools/chats/reminders/clear-chat-reminder.ts | 14 +++----------- .../tools/chats/reminders/set-chat-reminder.ts | 14 +++----------- .../mcp-server/src/tools/chats/search-chats.ts | 13 +++---------- .../src/tools/messages/send-message.ts | 13 +++---------- .../src/tools/top-level/open-in-app.ts | 13 +++---------- 9 files changed, 25 insertions(+), 88 deletions(-) diff --git a/.stats.yml b/.stats.yml index d64bab5..2621f51 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef -config_hash: d48fc12c89d2d812adf19d0508306f4a +config_hash: b43f460701263c30aba16a32385b20ed diff --git a/packages/mcp-server/src/tools/accounts/get-accounts.ts b/packages/mcp-server/src/tools/accounts/get-accounts.ts index dbc32c0..85fe44c 100644 --- a/packages/mcp-server/src/tools/accounts/get-accounts.ts +++ b/packages/mcp-server/src/tools/accounts/get-accounts.ts @@ -1,6 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -17,18 +16,10 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'get_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 accounts on this device.\n\n# Response Schema\n```json\n{\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 $defs: {\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\\').'\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: 'User the account belongs to.',\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```", + description: 'List connected accounts on this device.', 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/).', - }, - }, + properties: {}, required: [], }, annotations: { @@ -37,8 +28,7 @@ export const tool: Tool = { }; 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())); + return asTextContentResult(await client.accounts.list()); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/archive-chat.ts b/packages/mcp-server/src/tools/chats/archive-chat.ts index 9ec7869..c5eb924 100644 --- a/packages/mcp-server/src/tools/chats/archive-chat.ts +++ b/packages/mcp-server/src/tools/chats/archive-chat.ts @@ -1,6 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -17,8 +16,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'archive_chat', - 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.\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```", + description: 'Archive or unarchive a chat.', inputSchema: { type: 'object', properties: { @@ -31,12 +29,6 @@ export const tool: Tool = { 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'], }, @@ -44,8 +36,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { chatID, jq_filter, ...body } = args as any; - return asTextContentResult(await maybeFilter(jq_filter, await client.chats.archive(chatID, body))); + const { chatID, ...body } = args as any; + return asTextContentResult(await client.chats.archive(chatID, body)); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/get-chat.ts b/packages/mcp-server/src/tools/chats/get-chat.ts index 2e21624..9ab3537 100644 --- a/packages/mcp-server/src/tools/chats/get-chat.ts +++ b/packages/mcp-server/src/tools/chats/get-chat.ts @@ -1,6 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -17,8 +16,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'get_chat', - 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\nGet chat details: metadata, participants (limited), last activity.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/chat',\n $defs: {\n chat: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier of the chat (room/thread ID, same as id) across Beeper.'\n },\n accountID: {\n type: 'string',\n description: 'Beeper account ID this chat belongs to.'\n },\n network: {\n type: 'string',\n description: 'Display-only human-readable network name (e.g., \\'WhatsApp\\', \\'Messenger\\').'\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.',\n enum: [ 'single',\n 'group'\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 localChatID: {\n type: 'string',\n description: 'Local chat ID specific to this Beeper Desktop installation.'\n }\n },\n required: [ 'id',\n 'accountID',\n 'network',\n 'participants',\n 'title',\n 'type',\n 'unreadCount'\n ]\n },\n user: {\n type: 'object',\n description: 'User the account belongs to.',\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```", + description: 'Get chat details: metadata, participants (limited), last activity.', inputSchema: { type: 'object', properties: { @@ -32,12 +30,6 @@ export const tool: Tool = { 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'], }, @@ -47,8 +39,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { chatID, jq_filter, ...body } = args as any; - return asTextContentResult(await maybeFilter(jq_filter, await client.chats.retrieve(chatID, body))); + const { chatID, ...body } = args as any; + return asTextContentResult(await client.chats.retrieve(chatID, body)); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts index 4a96e0c..c7bd13d 100644 --- a/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts +++ b/packages/mcp-server/src/tools/chats/reminders/clear-chat-reminder.ts @@ -1,6 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -17,8 +16,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'clear_chat_reminder', - 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 a chat reminder.\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```", + description: 'Clear a chat reminder.', inputSchema: { type: 'object', properties: { @@ -27,12 +25,6 @@ export const tool: Tool = { description: 'The identifier of the chat to clear reminder from (accepts both chatID and local chat ID)', }, - 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'], }, @@ -42,8 +34,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { chatID, jq_filter, ...body } = args as any; - return asTextContentResult(await maybeFilter(jq_filter, await client.chats.reminders.delete(chatID))); + const { chatID, ...body } = args as any; + return asTextContentResult(await client.chats.reminders.delete(chatID)); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts index 41d8510..b311372 100644 --- a/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts +++ b/packages/mcp-server/src/tools/chats/reminders/set-chat-reminder.ts @@ -1,6 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -17,8 +16,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'set_chat_reminder', - 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```", + description: 'Set a reminder for a chat at a specific time.', inputSchema: { type: 'object', properties: { @@ -41,12 +39,6 @@ export const tool: Tool = { }, 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'], }, @@ -54,8 +46,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { chatID, jq_filter, ...body } = args as any; - return asTextContentResult(await maybeFilter(jq_filter, await client.chats.reminders.create(chatID, body))); + const { chatID, ...body } = args as any; + return asTextContentResult(await client.chats.reminders.create(chatID, body)); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/chats/search-chats.ts b/packages/mcp-server/src/tools/chats/search-chats.ts index 394192e..bfe61d7 100644 --- a/packages/mcp-server/src/tools/chats/search-chats.ts +++ b/packages/mcp-server/src/tools/chats/search-chats.ts @@ -1,6 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -18,7 +17,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'search_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 chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'.\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n hasMore: {\n type: 'boolean',\n description: 'True if additional results can be fetched using the provided cursors.'\n },\n items: {\n type: 'array',\n description: 'Chats matching the filters.',\n items: {\n $ref: '#/$defs/chat'\n }\n },\n newestCursor: {\n type: 'string',\n description: 'Cursor for fetching newer results (use with direction=\\'after\\'). Opaque string; do not inspect.'\n },\n oldestCursor: {\n type: 'string',\n description: 'Cursor for fetching older results (use with direction=\\'before\\'). Opaque string; do not inspect.'\n }\n },\n required: [ 'hasMore',\n 'items',\n 'newestCursor',\n 'oldestCursor'\n ],\n $defs: {\n chat: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier of the chat (room/thread ID, same as id) across Beeper.'\n },\n accountID: {\n type: 'string',\n description: 'Beeper account ID this chat belongs to.'\n },\n network: {\n type: 'string',\n description: 'Display-only human-readable network name (e.g., \\'WhatsApp\\', \\'Messenger\\').'\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.',\n enum: [ 'single',\n 'group'\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 localChatID: {\n type: 'string',\n description: 'Local chat ID specific to this Beeper Desktop installation.'\n }\n },\n required: [ 'id',\n 'accountID',\n 'network',\n 'participants',\n 'title',\n 'type',\n 'unreadCount'\n ]\n },\n user: {\n type: 'object',\n description: 'User the account belongs to.',\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```", + "Search chats by title/network or participants using Beeper Desktop's renderer algorithm. Optional 'scope'.", inputSchema: { type: 'object', properties: { @@ -88,12 +87,6 @@ export const tool: Tool = { 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: [], }, @@ -103,9 +96,9 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; + const body = args as any; const response = await client.chats.search(body).asResponse(); - return asTextContentResult(await maybeFilter(jq_filter, await response.json())); + return asTextContentResult(await response.json()); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/messages/send-message.ts b/packages/mcp-server/src/tools/messages/send-message.ts index ee26fae..60668a7 100644 --- a/packages/mcp-server/src/tools/messages/send-message.ts +++ b/packages/mcp-server/src/tools/messages/send-message.ts @@ -1,6 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -18,7 +17,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'send_message', 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 allOf: [ {\n $ref: '#/$defs/base_response'\n }\n ],\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```", + 'Send a text message to a specific chat. Supports replying to existing messages. Returns the sent message ID and a deeplink to the chat', inputSchema: { type: 'object', properties: { @@ -34,12 +33,6 @@ export const tool: Tool = { 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'], }, @@ -47,8 +40,8 @@ export const tool: Tool = { }; 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))); + const body = args as any; + return asTextContentResult(await client.messages.send(body)); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/top-level/open-in-app.ts b/packages/mcp-server/src/tools/top-level/open-in-app.ts index aa49f0d..d7cac93 100644 --- a/packages/mcp-server/src/tools/top-level/open-in-app.ts +++ b/packages/mcp-server/src/tools/top-level/open-in-app.ts @@ -1,6 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { maybeFilter } from '@beeper/desktop-mcp/filtering'; import { Metadata, asTextContentResult } from '@beeper/desktop-mcp/tools/types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -18,7 +17,7 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'open_in_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\nOpen Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment.\n\n# Response Schema\n```json\n{\n type: 'object',\n description: 'Response indicating successful app opening.',\n properties: {\n success: {\n type: 'boolean',\n description: 'Whether the app was successfully opened/focused.'\n }\n },\n required: [ 'success'\n ]\n}\n```", + 'Open Beeper Desktop and optionally navigate to a specific chat, message, or pre-fill draft text and attachment.', inputSchema: { type: 'object', properties: { @@ -39,12 +38,6 @@ export const tool: Tool = { type: 'string', description: 'Optional message ID. Jumps to that message in the chat when opening.', }, - 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: [], }, @@ -52,8 +45,8 @@ export const tool: Tool = { }; export const handler = async (client: BeeperDesktop, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - return asTextContentResult(await maybeFilter(jq_filter, await client.open(body))); + const body = args as any; + return asTextContentResult(await client.open(body)); }; export default { metadata, tool, handler }; From 2e7863c39d0b4c3f9254f0f1bac5003c6dd52370 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:32:21 +0000 Subject: [PATCH 32/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 2621f51..103ad16 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef -config_hash: b43f460701263c30aba16a32385b20ed +config_hash: 738402ade5ac9528c8ef1677aa1d70f7 From 04751e2fe6cc2dac4ac65b4ebef55e6d573fa15b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:35:48 +0000 Subject: [PATCH 33/36] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 103ad16..8831e71 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-953cbc1ea1fe675bf2d32b18030a3ac509c521946921cb338c0d1c2cfef89424.yml -openapi_spec_hash: b4d08ca2dc21bc00245c9c9408be89ef +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml +openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765 config_hash: 738402ade5ac9528c8ef1677aa1d70f7 From 33bbc6bde07fa0c74c09266445d88ef63f20ca6d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:19:53 +0000 Subject: [PATCH 34/36] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 8831e71..baf1a99 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765 -config_hash: 738402ade5ac9528c8ef1677aa1d70f7 +config_hash: fc42f6a9efd6f34ca68f1c4328272acf From c1451540df373793b5fd90e780faab8acf38a1dc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:17:30 +0000 Subject: [PATCH 35/36] feat(api): remove limit from list routes --- .stats.yml | 6 +- api.md | 8 +-- src/client.ts | 23 +++++--- src/core/pagination.ts | 75 +++++++++++++++++++++++-- src/resources/chats/chats.ts | 28 +++++---- src/resources/chats/index.ts | 4 +- src/resources/index.ts | 4 +- src/resources/messages.ts | 29 ++++++---- src/resources/shared.ts | 6 +- tests/api-resources/chats/chats.test.ts | 1 - tests/api-resources/messages.test.ts | 1 - 11 files changed, 138 insertions(+), 47 deletions(-) diff --git a/.stats.yml b/.stats.yml index baf1a99..c2693cc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-803a4423d75f7a43582319924f0770153fd5ec313b9466c290513b9a891c2653.yml -openapi_spec_hash: f32dfbf172bb043fd8c961cba5f73765 -config_hash: fc42f6a9efd6f34ca68f1c4328272acf +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-a3fb0de6dd98f8a51d73e3fdf51de6143f2e8e764048246392624a56b4a3a481.yml +openapi_spec_hash: 50e1001c340cb0bd3436b6329240769b +config_hash: 2e31d02f28a11ef29eb747bcf559786a diff --git a/api.md b/api.md index 19e98b5..4c59aba 100644 --- a/api.md +++ b/api.md @@ -56,9 +56,9 @@ Methods: - client.chats.create({ ...params }) -> ChatCreateResponse - client.chats.retrieve(chatID, { ...params }) -> Chat -- client.chats.list({ ...params }) -> ChatListResponsesCursor +- client.chats.list({ ...params }) -> ChatListResponsesCursorList - client.chats.archive(chatID, { ...params }) -> BaseResponse -- client.chats.search({ ...params }) -> ChatsCursor +- client.chats.search({ ...params }) -> ChatsCursorSearch ## Reminders @@ -75,6 +75,6 @@ Types: Methods: -- client.messages.list({ ...params }) -> MessagesCursor -- client.messages.search({ ...params }) -> MessagesCursor +- client.messages.list({ ...params }) -> MessagesCursorList +- client.messages.search({ ...params }) -> MessagesCursorSearch - client.messages.send({ ...params }) -> MessageSendResponse diff --git a/src/client.ts b/src/client.ts index 205ef99..cb8bdcf 100644 --- a/src/client.ts +++ b/src/client.ts @@ -15,7 +15,13 @@ import * as qs from './internal/qs'; import { VERSION } from './version'; import * as Errors from './core/error'; import * as Pagination from './core/pagination'; -import { AbstractPage, type CursorParams, CursorResponse } from './core/pagination'; +import { + AbstractPage, + type CursorListParams, + CursorListResponse, + type CursorSearchParams, + CursorSearchResponse, +} from './core/pagination'; import * as Uploads from './core/uploads'; import * as API from './resources/index'; import * as TopLevelAPI from './resources/top-level'; @@ -44,11 +50,11 @@ import { ChatCreateResponse, ChatListParams, ChatListResponse, - ChatListResponsesCursor, + ChatListResponsesCursorList, ChatRetrieveParams, ChatSearchParams, Chats, - ChatsCursor, + ChatsCursorSearch, } from './resources/chats/chats'; import { type Fetch } from './internal/builtin-types'; import { isRunningInBrowser } from './internal/detect-platform'; @@ -838,8 +844,11 @@ BeeperDesktop.Messages = Messages; export declare namespace BeeperDesktop { export type RequestOptions = Opts.RequestOptions; - export import Cursor = Pagination.Cursor; - export { type CursorParams as CursorParams, type CursorResponse as CursorResponse }; + export import CursorSearch = Pagination.CursorSearch; + export { type CursorSearchParams as CursorSearchParams, type CursorSearchResponse as CursorSearchResponse }; + + export import CursorList = Pagination.CursorList; + export { type CursorListParams as CursorListParams, type CursorListResponse as CursorListResponse }; export { type DownloadAssetResponse as DownloadAssetResponse, @@ -863,8 +872,8 @@ export declare namespace BeeperDesktop { type Chat as Chat, type ChatCreateResponse as ChatCreateResponse, type ChatListResponse as ChatListResponse, - type ChatListResponsesCursor as ChatListResponsesCursor, - type ChatsCursor as ChatsCursor, + type ChatListResponsesCursorList as ChatListResponsesCursorList, + type ChatsCursorSearch as ChatsCursorSearch, type ChatCreateParams as ChatCreateParams, type ChatRetrieveParams as ChatRetrieveParams, type ChatListParams as ChatListParams, diff --git a/src/core/pagination.ts b/src/core/pagination.ts index 00238cf..11e94e9 100644 --- a/src/core/pagination.ts +++ b/src/core/pagination.ts @@ -107,7 +107,7 @@ export class PagePromise< } } -export interface CursorResponse { +export interface CursorSearchResponse { items: Array; hasMore: boolean; @@ -117,7 +117,7 @@ export interface CursorResponse { newestCursor: string | null; } -export interface CursorParams { +export interface CursorSearchParams { cursor?: string | null; direction?: string | null; @@ -125,7 +125,7 @@ export interface CursorParams { limit?: number | null; } -export class Cursor extends AbstractPage implements CursorResponse { +export class CursorSearch extends AbstractPage implements CursorSearchResponse { items: Array; hasMore: boolean; @@ -137,7 +137,74 @@ export class Cursor extends AbstractPage implements CursorResponse, + body: CursorSearchResponse, + options: FinalRequestOptions, + ) { + super(client, response, body, options); + + this.items = body.items || []; + this.hasMore = body.hasMore || false; + this.oldestCursor = body.oldestCursor || null; + this.newestCursor = body.newestCursor || null; + } + + getPaginatedItems(): Item[] { + return this.items ?? []; + } + + override hasNextPage(): boolean { + if (this.hasMore === false) { + return false; + } + + return super.hasNextPage(); + } + + nextPageRequestOptions(): PageRequestOptions | null { + const cursor = this.oldestCursor; + if (!cursor) { + return null; + } + + return { + ...this.options, + query: { + ...maybeObj(this.options.query), + cursor, + }, + }; + } +} + +export interface CursorListResponse { + items: Array; + + hasMore: boolean; + + oldestCursor: string | null; + + newestCursor: string | null; +} + +export interface CursorListParams { + cursor?: string | null; + + direction?: string | null; +} + +export class CursorList extends AbstractPage implements CursorListResponse { + items: Array; + + hasMore: boolean; + + oldestCursor: string | null; + + newestCursor: string | null; + + constructor( + client: BeeperDesktop, + response: Response, + body: CursorListResponse, options: FinalRequestOptions, ) { super(client, response, body, options); diff --git a/src/resources/chats/chats.ts b/src/resources/chats/chats.ts index c91dacc..271d168 100644 --- a/src/resources/chats/chats.ts +++ b/src/resources/chats/chats.ts @@ -5,7 +5,13 @@ import * as Shared from '../shared'; import * as RemindersAPI from './reminders'; import { ReminderCreateParams, Reminders } from './reminders'; import { APIPromise } from '../../core/api-promise'; -import { Cursor, type CursorParams, PagePromise } from '../../core/pagination'; +import { + CursorList, + type CursorListParams, + CursorSearch, + type CursorSearchParams, + PagePromise, +} from '../../core/pagination'; import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; @@ -66,8 +72,8 @@ export class Chats extends APIResource { list( query: ChatListParams | null | undefined = {}, options?: RequestOptions, - ): PagePromise { - return this._client.getAPIList('/v1/chats', Cursor, { query, ...options }); + ): PagePromise { + return this._client.getAPIList('/v1/chats', CursorList, { query, ...options }); } /** @@ -104,14 +110,14 @@ export class Chats extends APIResource { search( query: ChatSearchParams | null | undefined = {}, options?: RequestOptions, - ): PagePromise { - return this._client.getAPIList('/v1/chats/search', Cursor, { query, ...options }); + ): PagePromise { + return this._client.getAPIList('/v1/chats/search', CursorSearch, { query, ...options }); } } -export type ChatListResponsesCursor = Cursor; +export type ChatListResponsesCursorList = CursorList; -export type ChatsCursor = Cursor; +export type ChatsCursorSearch = CursorSearch; export interface Chat { /** @@ -253,7 +259,7 @@ export interface ChatRetrieveParams { maxParticipantCount?: number | null; } -export interface ChatListParams extends CursorParams { +export interface ChatListParams extends CursorListParams { /** * Limit to specific account IDs. If omitted, fetches from all accounts. */ @@ -267,7 +273,7 @@ export interface ChatArchiveParams { archived?: boolean; } -export interface ChatSearchParams extends CursorParams { +export interface ChatSearchParams extends CursorSearchParams { /** * Provide an array of account IDs to filter chats from specific messaging accounts * only @@ -329,8 +335,8 @@ export declare namespace Chats { type Chat as Chat, type ChatCreateResponse as ChatCreateResponse, type ChatListResponse as ChatListResponse, - type ChatListResponsesCursor as ChatListResponsesCursor, - type ChatsCursor as ChatsCursor, + type ChatListResponsesCursorList as ChatListResponsesCursorList, + type ChatsCursorSearch as ChatsCursorSearch, type ChatCreateParams as ChatCreateParams, type ChatRetrieveParams as ChatRetrieveParams, type ChatListParams as ChatListParams, diff --git a/src/resources/chats/index.ts b/src/resources/chats/index.ts index 16c70b2..25bb4a5 100644 --- a/src/resources/chats/index.ts +++ b/src/resources/chats/index.ts @@ -10,7 +10,7 @@ export { type ChatListParams, type ChatArchiveParams, type ChatSearchParams, - type ChatListResponsesCursor, - type ChatsCursor, + type ChatListResponsesCursorList, + type ChatsCursorSearch, } from './chats'; export { Reminders, type ReminderCreateParams } from './reminders'; diff --git a/src/resources/index.ts b/src/resources/index.ts index 264800e..74575e3 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -12,8 +12,8 @@ export { type ChatListParams, type ChatArchiveParams, type ChatSearchParams, - type ChatListResponsesCursor, - type ChatsCursor, + type ChatListResponsesCursorList, + type ChatsCursorSearch, } from './chats/chats'; export { Contacts, type ContactSearchResponse, type ContactSearchParams } from './contacts'; export { diff --git a/src/resources/messages.ts b/src/resources/messages.ts index c48b803..da415dc 100644 --- a/src/resources/messages.ts +++ b/src/resources/messages.ts @@ -2,9 +2,15 @@ import { APIResource } from '../core/resource'; import * as Shared from './shared'; -import { MessagesCursor } from './shared'; +import { MessagesCursorList, MessagesCursorSearch } from './shared'; import { APIPromise } from '../core/api-promise'; -import { Cursor, type CursorParams, PagePromise } from '../core/pagination'; +import { + CursorList, + type CursorListParams, + CursorSearch, + type CursorSearchParams, + PagePromise, +} from '../core/pagination'; import { RequestOptions } from '../internal/request-options'; /** @@ -24,8 +30,8 @@ export class Messages extends APIResource { * } * ``` */ - list(query: MessageListParams, options?: RequestOptions): PagePromise { - return this._client.getAPIList('/v1/messages', Cursor, { query, ...options }); + list(query: MessageListParams, options?: RequestOptions): PagePromise { + return this._client.getAPIList('/v1/messages', CursorList, { query, ...options }); } /** @@ -42,8 +48,11 @@ export class Messages extends APIResource { search( query: MessageSearchParams | null | undefined = {}, options?: RequestOptions, - ): PagePromise { - return this._client.getAPIList('/v1/messages/search', Cursor, { query, ...options }); + ): PagePromise { + return this._client.getAPIList('/v1/messages/search', CursorSearch, { + query, + ...options, + }); } /** @@ -74,14 +83,14 @@ export interface MessageSendResponse extends Shared.BaseResponse { pendingMessageID: string; } -export interface MessageListParams extends CursorParams { +export interface MessageListParams extends CursorListParams { /** - * The chat ID to list messages from + * Chat ID to list messages from */ chatID: string; } -export interface MessageSearchParams extends CursorParams { +export interface MessageSearchParams extends CursorSearchParams { /** * Limit search to specific account IDs. */ @@ -168,4 +177,4 @@ export declare namespace Messages { }; } -export { type MessagesCursor }; +export { type MessagesCursorList, type MessagesCursorSearch }; diff --git a/src/resources/shared.ts b/src/resources/shared.ts index 98ffebb..716ddb7 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Cursor } from '../core/pagination'; +import { CursorList, CursorSearch } from '../core/pagination'; export interface Attachment { /** @@ -258,4 +258,6 @@ export interface User { username?: string; } -export type MessagesCursor = Cursor; +export type MessagesCursorList = CursorList; + +export type MessagesCursorSearch = CursorSearch; diff --git a/tests/api-resources/chats/chats.test.ts b/tests/api-resources/chats/chats.test.ts index 9e1cd5e..c411418 100644 --- a/tests/api-resources/chats/chats.test.ts +++ b/tests/api-resources/chats/chats.test.ts @@ -78,7 +78,6 @@ describe('resource chats', () => { ], cursor: '1725489123456', direction: 'before', - limit: 1, }, { path: '/_stainless_unknown_path' }, ), diff --git a/tests/api-resources/messages.test.ts b/tests/api-resources/messages.test.ts index 33d4c7a..616bd17 100644 --- a/tests/api-resources/messages.test.ts +++ b/tests/api-resources/messages.test.ts @@ -24,7 +24,6 @@ describe('resource messages', () => { chatID: '!NCdzlIaMjZUmvmvyHU:beeper.com', cursor: '821744079', direction: 'before', - limit: 1, }); }); From d434b3c34bdc2b695182e6ba3235c056aeffe063 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:17:54 +0000 Subject: [PATCH 36/36] release: 0.1.6 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++ package.json | 2 +- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/server.ts | 2 +- src/version.ts | 2 +- 6 files changed, 54 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 18e45d5..0f24e47 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.5" + ".": "0.1.6" } diff --git a/CHANGELOG.md b/CHANGELOG.md index fce55f0..5d424b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## 0.1.6 (2025-10-07) + +Full Changelog: [v0.1.5...v0.1.6](https://github.com/beeper/desktop-api-js/compare/v0.1.5...v0.1.6) + +### Features + +* **api:** manual updates ([e12ab16](https://github.com/beeper/desktop-api-js/commit/e12ab16a4664ef6b770750225dc0e9ea53005b89)) +* **api:** manual updates ([914badc](https://github.com/beeper/desktop-api-js/commit/914badc5d98d1b3ab5b7d1b404c687110ef34913)) +* **api:** manual updates ([e657efe](https://github.com/beeper/desktop-api-js/commit/e657efe6bf48b6c76e09060ac00c8e0403213982)) +* **api:** manual updates ([0de85a3](https://github.com/beeper/desktop-api-js/commit/0de85a39f784415474e73187609e4ef4c5fe2cf1)) +* **api:** manual updates ([d7c35d2](https://github.com/beeper/desktop-api-js/commit/d7c35d27538be9752a6e15a4d17c4b5354b44b3f)) +* **api:** manual updates ([80f8804](https://github.com/beeper/desktop-api-js/commit/80f880478414edcba479e4717c5a1033a2545d3d)) +* **api:** manual updates ([e404d7e](https://github.com/beeper/desktop-api-js/commit/e404d7ebd08830f115f7468598cc275db637d508)) +* **api:** manual updates ([540c168](https://github.com/beeper/desktop-api-js/commit/540c168bfa982178e5ab743c9ef3986784438617)) +* **api:** manual updates ([3501ff9](https://github.com/beeper/desktop-api-js/commit/3501ff900847979fcf7b6e543a99364b74a979a5)) +* **api:** manual updates ([d1a5970](https://github.com/beeper/desktop-api-js/commit/d1a5970f917e9ddfe1060a4129feee7bb1be1414)) +* **api:** remove limit from list routes ([c145154](https://github.com/beeper/desktop-api-js/commit/c1451540df373793b5fd90e780faab8acf38a1dc)) +* **mcp:** add option for including docs tools ([a521cb8](https://github.com/beeper/desktop-api-js/commit/a521cb8f972778c0253b657b712da7681dd78853)) +* **mcp:** enable experimental docs search tool ([c349699](https://github.com/beeper/desktop-api-js/commit/c349699a05745356ce3d12dc693d643a85a52d5d)) + + +### Bug Fixes + +* **mcp:** fix cli argument parsing logic ([98c6489](https://github.com/beeper/desktop-api-js/commit/98c6489b8780dbe7d4ba377fb64364fb862fc7f7)) +* **mcp:** resolve a linting issue in server code ([2c84441](https://github.com/beeper/desktop-api-js/commit/2c8444166c9b4e6f67c9bf4827989aee65d672e8)) + + +### Performance Improvements + +* faster formatting ([a4321bf](https://github.com/beeper/desktop-api-js/commit/a4321bf9c91f3043414724486c130f3b0bd25606)) + + +### Chores + +* configure new SDK language ([f50310b](https://github.com/beeper/desktop-api-js/commit/f50310b4a7cc3e2286430530c8ffc867f77f7ef9)) +* configure new SDK language ([fee94ba](https://github.com/beeper/desktop-api-js/commit/fee94ba4b23ec91abc030ddbb872c08af43e2df8)) +* do not install brew dependencies in ./scripts/bootstrap by default ([cd62f83](https://github.com/beeper/desktop-api-js/commit/cd62f83fc5e6e93501df72cf49c0ee2838e02476)) +* fix changelog with actual entries ([45de3f5](https://github.com/beeper/desktop-api-js/commit/45de3f5a1d86486b9752e32cbd5283d6efe88434)) +* **internal:** codegen related update ([3598172](https://github.com/beeper/desktop-api-js/commit/3598172f0839f111c6000c9f29502d21afabee0c)) +* **internal:** codegen related update ([44137d6](https://github.com/beeper/desktop-api-js/commit/44137d644ffcd7407acc815a68d8df093e17f45d)) +* **internal:** fix incremental formatting in some cases ([697d742](https://github.com/beeper/desktop-api-js/commit/697d742608fead14c628f5d809b6a4beedff4656)) +* **internal:** ignore .eslintcache ([22d7f29](https://github.com/beeper/desktop-api-js/commit/22d7f29cd477bf4d22c68b51cd4b99bcf710efbd)) +* **internal:** remove .eslintcache ([f2a0dfb](https://github.com/beeper/desktop-api-js/commit/f2a0dfbd7930292f715ab530a6dedbf53e6ae8e3)) +* **internal:** remove deprecated `compilerOptions.baseUrl` from tsconfig.json ([ffaa6d6](https://github.com/beeper/desktop-api-js/commit/ffaa6d646236e88ecf997dc9a0b77733029a885b)) +* **internal:** use npm pack for build uploads ([31e0341](https://github.com/beeper/desktop-api-js/commit/31e0341efbad7c4b9abc13d145c477eb3676cc0b)) +* **jsdoc:** fix [@link](https://github.com/link) annotations to refer only to parts of the package‘s public interface ([8cf89a9](https://github.com/beeper/desktop-api-js/commit/8cf89a9e31969a45d24179342955b2a4e931ae6f)) +* **mcp:** allow pointing `docs_search` tool at other URLs ([6044c34](https://github.com/beeper/desktop-api-js/commit/6044c3467dd578e2fba46fa10f83ad3240222c3f)) +* update lockfile ([a9a736f](https://github.com/beeper/desktop-api-js/commit/a9a736fba58dde3c39bc2f5501be1ae30f380367)) + ## 0.1.5 (2025-09-19) Full Changelog: [v0.1.4...v0.1.5](https://github.com/beeper/desktop-api-js/compare/v0.1.4...v0.1.5) diff --git a/package.json b/package.json index bf401d4..f94972f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@beeper/desktop-api", - "version": "0.1.5", + "version": "0.1.6", "description": "The official TypeScript library for the Beeper Desktop API", "author": "Beeper Desktop ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index b9510db..04c01af 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@beeper/desktop-mcp", - "version": "0.1.5", + "version": "0.1.6", "description": "The official MCP Server for the Beeper Desktop API", "author": "Beeper Desktop ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 5b30ecb..314817c 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -34,7 +34,7 @@ export const newMcpServer = () => new McpServer( { name: 'beeper_desktop_api_api', - version: '0.1.5', + version: '0.1.6', }, { capabilities: { tools: {}, logging: {} }, diff --git a/src/version.ts b/src/version.ts index 44ea521..8243372 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.1.5'; // x-release-please-version +export const VERSION = '0.1.6'; // x-release-please-version