diff --git a/.github/workflows/generate-toolkit-docs.md b/.github/workflows/generate-toolkit-docs.md index 19d18ba95..6b67db9eb 100644 --- a/.github/workflows/generate-toolkit-docs.md +++ b/.github/workflows/generate-toolkit-docs.md @@ -13,7 +13,7 @@ This workflow regenerates toolkit JSON and opens a PR with the changes. It can b Required secrets: -- `ENGINE_API_URL` +- `ENGINE_API_URL` (public catalog API host) - `ANTHROPIC_API_KEY` Optional secrets: diff --git a/.github/workflows/generate-toolkit-docs.yml b/.github/workflows/generate-toolkit-docs.yml index bbed4a76b..a868d309c 100644 --- a/.github/workflows/generate-toolkit-docs.yml +++ b/.github/workflows/generate-toolkit-docs.yml @@ -66,7 +66,7 @@ jobs: --preserve-last-known-good \ --verbose \ --api-source public-catalog \ - --tool-metadata-url "$ENGINE_API_URL" \ + --api-url "$ARCADE_API_URL" \ --llm-provider anthropic \ --llm-model "$ANTHROPIC_MODEL" \ --llm-api-key "$ANTHROPIC_API_KEY" \ @@ -84,7 +84,7 @@ jobs: echo "succeeded=true" >>"$GITHUB_OUTPUT" working-directory: toolkit-docs-generator env: - ENGINE_API_URL: ${{ secrets.ENGINE_API_URL }} + ARCADE_API_URL: ${{ secrets.ENGINE_API_URL }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_MODEL: ${{ secrets.ANTHROPIC_MODEL || 'claude-sonnet-4-6' }} # Stronger model for the secret-coherence editor. Keeps diff --git a/toolkit-docs-generator/ARCHITECTURE.md b/toolkit-docs-generator/ARCHITECTURE.md index c1a697b78..165666958 100644 --- a/toolkit-docs-generator/ARCHITECTURE.md +++ b/toolkit-docs-generator/ARCHITECTURE.md @@ -67,8 +67,6 @@ it. The sidebar sync writes navigation only, and never touches toolkit JSON. ### Data sources - `PublicCatalogApiSource` fetches tool metadata from the Engine public catalog API. -- `EngineApiSource` fetches tool metadata from the authenticated Engine API (deprecated). -- `ArcadeApiSource` fetches tool metadata from the Arcade API (deprecated). - `DesignSystemMetadataSource` loads toolkit metadata from `@arcadeai/design-system`. - `MarkdownCurationSource` compiles documentation chunks, import declarations, and subpages from the configured curation directory. When configured, that @@ -140,7 +138,6 @@ public, read-only values configured through these Vercel environment variables: ## Key files - `src/sources/public-catalog-api.ts` — tool metadata from Engine public catalog -- `src/sources/engine-api.ts` — tool metadata from authenticated Engine API (deprecated) - `src/sources/markdown-curation.ts` — Markdown and MDX curation compiler ([format reference](CURATION.md)) - `src/sources/toolkit-data-source.ts` — unified data source diff --git a/toolkit-docs-generator/CURATION.md b/toolkit-docs-generator/CURATION.md index 3b14d09f6..f92a1b671 100644 --- a/toolkit-docs-generator/CURATION.md +++ b/toolkit-docs-generator/CURATION.md @@ -251,8 +251,7 @@ generation PR, or generate that one toolkit with Engine credentials and run ```bash ../node_modules/.bin/tsx src/cli/index.ts generate \ --providers "GoogleFlights" \ - --tool-metadata-url "$ENGINE_API_URL" \ - --tool-metadata-key "$ENGINE_API_KEY" \ + --api-url "$ARCADE_API_URL" \ --custom-sections ./curation \ --skip-examples --skip-summary --skip-secret-coherence \ --output data/toolkits diff --git a/toolkit-docs-generator/README.md b/toolkit-docs-generator/README.md index 1f6d0f6a5..858baae89 100644 --- a/toolkit-docs-generator/README.md +++ b/toolkit-docs-generator/README.md @@ -36,7 +36,7 @@ It runs these steps: Required secrets: -- `ENGINE_API_URL` +- `ENGINE_API_URL` (API host for the public catalog; rename to `ARCADE_API_URL` in a follow-up) - `ANTHROPIC_API_KEY` for examples, summaries, and secret-coherence edits Optional secrets: @@ -107,8 +107,7 @@ The summary generator is configured to **never list OAuth scopes** in the genera ```bash pnpm dlx tsx src/cli/index.ts generate \ --providers "Github" \ - --tool-metadata-url "$ENGINE_API_URL" \ - --tool-metadata-key "$ENGINE_API_KEY" \ + --api-url "$ARCADE_API_URL" \ --llm-provider openai \ --llm-model gpt-4.1-mini \ --llm-api-key "$OPENAI_API_KEY" \ @@ -138,8 +137,7 @@ Generate a single toolkit: ```bash pnpm dlx tsx src/cli/index.ts generate \ --providers "Github:1.0.0" \ - --tool-metadata-url "$ENGINE_API_URL" \ - --tool-metadata-key "$ENGINE_API_KEY" \ + --api-url "$ARCADE_API_URL" \ --llm-provider openai \ --llm-model gpt-4.1-mini \ --llm-api-key "$OPENAI_API_KEY" \ @@ -152,8 +150,7 @@ Generate all toolkits: pnpm dlx tsx src/cli/index.ts generate \ --all \ --skip-unchanged \ - --tool-metadata-url "$ENGINE_API_URL" \ - --tool-metadata-key "$ENGINE_API_KEY" \ + --api-url "$ARCADE_API_URL" \ --llm-provider openai \ --llm-model gpt-4.1-mini \ --llm-api-key "$OPENAI_API_KEY" \ @@ -165,8 +162,7 @@ Generate without LLM output: ```bash pnpm dlx tsx src/cli/index.ts generate \ --providers "Asana:0.1.3" \ - --tool-metadata-url "$ENGINE_API_URL" \ - --tool-metadata-key "$ENGINE_API_KEY" \ + --api-url "$ARCADE_API_URL" \ --skip-examples \ --skip-summary \ --output data/toolkits @@ -231,8 +227,7 @@ deletes it and rebuilds `index.json`. - `--all` generate all toolkits - `--providers` generate a subset of toolkits - `--skip-unchanged` only write changed toolkits -- `--api-source` select `public-catalog` (default with `ENGINE_API_URL`), `tool-metadata` - (deprecated; requires `ENGINE_API_KEY`), `list-tools` (deprecated), or `mock` +- `--api-source` select `public-catalog` (default with `ARCADE_API_URL` or `ENGINE_API_URL`) or `mock` - `--previous-output` compare against a previous output directory - `--custom-sections` load an authoritative Markdown/MDX curation directory - `--skip-examples`, `--skip-summary` disable LLM steps diff --git a/toolkit-docs-generator/src/cli/api-source.ts b/toolkit-docs-generator/src/cli/api-source.ts index c52cfca01..bea507402 100644 --- a/toolkit-docs-generator/src/cli/api-source.ts +++ b/toolkit-docs-generator/src/cli/api-source.ts @@ -1,21 +1,13 @@ -export type ApiSource = - | "public-catalog" - | "list-tools" - | "tool-metadata" - | "mock"; +export type ApiSource = "public-catalog" | "mock"; type ApiSourceOptions = { apiSource?: string; - toolMetadataUrl?: string; - toolMetadataKey?: string; + apiUrl?: string; }; const EXPLICIT_API_SOURCES: Record = { "public-catalog": "public-catalog", public: "public-catalog", - "list-tools": "list-tools", - engine: "tool-metadata", - "tool-metadata": "tool-metadata", mock: "mock", }; @@ -26,29 +18,23 @@ const resolveExplicitApiSource = (apiSource: string): ApiSource => { } throw new Error( - `Invalid --api-source "${apiSource}". Use "public-catalog", "list-tools", "tool-metadata", or "mock".` + `Invalid --api-source "${apiSource}". Use "public-catalog" or "mock".` ); }; const resolveAutoDetectedApiSource = (options: ApiSourceOptions): ApiSource => { - const hasToolMetadataKey = !!( - options.toolMetadataKey ?? process.env.ENGINE_API_KEY - ); - const hasToolMetadataUrl = !!( - options.toolMetadataUrl ?? process.env.ENGINE_API_URL - ); - - if (hasToolMetadataKey && hasToolMetadataUrl) { - return "tool-metadata"; - } + const hasApiUrl = !!(options.apiUrl ?? resolveApiBaseUrlFromEnv()); - if (hasToolMetadataUrl) { + if (hasApiUrl) { return "public-catalog"; } return "mock"; }; +export const resolveApiBaseUrlFromEnv = (): string | undefined => + process.env.ARCADE_API_URL ?? process.env.ENGINE_API_URL; + export const resolveApiSource = (options: ApiSourceOptions): ApiSource => { if (options.apiSource) { return resolveExplicitApiSource(options.apiSource); @@ -56,6 +42,3 @@ export const resolveApiSource = (options: ApiSourceOptions): ApiSource => { return resolveAutoDetectedApiSource(options); }; - -export const isDeprecatedApiSource = (apiSource: ApiSource): boolean => - apiSource === "tool-metadata" || apiSource === "list-tools"; diff --git a/toolkit-docs-generator/src/cli/index.ts b/toolkit-docs-generator/src/cli/index.ts index 8dde372a2..2ab2968d5 100644 --- a/toolkit-docs-generator/src/cli/index.ts +++ b/toolkit-docs-generator/src/cli/index.ts @@ -54,9 +54,7 @@ import { import { createMockMetadataSource } from "../sources/mock-metadata"; import { createDesignSystemProviderIdResolver } from "../sources/oauth-provider-resolver"; import { - createArcadeToolkitDataSource, createCachedToolkitDataSource, - createEngineToolkitDataSource, createMockToolkitDataSource, createPublicCatalogToolkitDataSource, type ToolkitData, @@ -86,7 +84,7 @@ import { } from "../utils/run-logs"; import { type ApiSource, - isDeprecatedApiSource, + resolveApiBaseUrlFromEnv, resolveApiSource, } from "./api-source"; import { cleanupExcludedToolkitOutput } from "./exclusion-cleanup"; @@ -484,36 +482,12 @@ const resolveSecretEditGenerator = ( interface ToolkitDataSourceOptions { apiSource?: string; - listToolsUrl?: string; - listToolsKey?: string; - listToolsPageSize?: number; - toolMetadataUrl?: string; - toolMetadataKey?: string; - toolMetadataPageSize?: number; + apiUrl?: string; + apiPageSize?: number; } -const resolveListToolsConfig = (options: ToolkitDataSourceOptions) => { - const baseUrl = - options.listToolsUrl ?? - process.env.ARCADE_API_URL ?? - "https://api.arcade.dev"; - const apiKey = options.listToolsKey ?? process.env.ARCADE_API_KEY; - - if (!apiKey) { - return null; - } - - return { - baseUrl, - apiKey, - ...(options.listToolsPageSize - ? { pageSize: options.listToolsPageSize } - : {}), - }; -}; - const resolvePublicCatalogConfig = (options: ToolkitDataSourceOptions) => { - const baseUrl = options.toolMetadataUrl ?? process.env.ENGINE_API_URL; + const baseUrl = options.apiUrl ?? resolveApiBaseUrlFromEnv(); if (!baseUrl) { return null; @@ -521,41 +495,10 @@ const resolvePublicCatalogConfig = (options: ToolkitDataSourceOptions) => { return { baseUrl, - ...(options.toolMetadataPageSize - ? { toolsPageSize: options.toolMetadataPageSize } - : {}), + ...(options.apiPageSize ? { toolsPageSize: options.apiPageSize } : {}), }; }; -const resolveToolMetadataConfig = (options: ToolkitDataSourceOptions) => { - const baseUrl = options.toolMetadataUrl ?? process.env.ENGINE_API_URL; - const apiKey = options.toolMetadataKey ?? process.env.ENGINE_API_KEY; - - if (!(baseUrl && apiKey)) { - return null; - } - - return { - baseUrl, - apiKey, - ...(options.toolMetadataPageSize - ? { pageSize: options.toolMetadataPageSize } - : {}), - }; -}; - -const warnDeprecatedApiSource = (apiSource: ApiSource): void => { - if (!isDeprecatedApiSource(apiSource)) { - return; - } - - console.warn( - chalk.yellow( - `Warning: --api-source ${apiSource} is deprecated. Use "public-catalog" instead.` - ) - ); -}; - const createPublicCatalogToolkitSource = ( options: ToolkitDataSourceOptions, metadataSource: ReturnType, @@ -564,7 +507,7 @@ const createPublicCatalogToolkitSource = ( const config = resolvePublicCatalogConfig(options); if (!config) { throw new Error( - "Public catalog API requires --tool-metadata-url (or ENGINE_API_URL environment variable)." + "Public catalog API requires --api-url (or ARCADE_API_URL / ENGINE_API_URL environment variable)." ); } if (verbose) { @@ -580,86 +523,25 @@ const createPublicCatalogToolkitSource = ( }); }; -const createListToolsToolkitSource = ( - options: ToolkitDataSourceOptions, - metadataSource: ReturnType, - verbose: boolean, - spinner?: ReturnType -): ToolkitDataSource => { - const config = resolveListToolsConfig(options); - if (!config) { - throw new Error( - "List tools API requires --list-tools-key (or ARCADE_API_KEY environment variable)." - ); - } - if (verbose) { - console.log(chalk.dim(`Using /v1/tools endpoint: ${config.baseUrl}`)); - } - const onProgress = spinner - ? (fetched: number, total: number) => { - spinner.text = `Fetching tools from API... ${fetched}/${total}`; - } - : undefined; - return createArcadeToolkitDataSource({ - arcade: { ...config, onProgress }, - metadataSource, - }); -}; - -const createToolMetadataToolkitSource = ( +const createToolkitDataSourceForApi = ( + apiSource: ApiSource, options: ToolkitDataSourceOptions, metadataSource: ReturnType, + mockDataDir: string, verbose: boolean ): ToolkitDataSource => { - const config = resolveToolMetadataConfig(options); - if (!config) { - throw new Error( - "Tool metadata API requires --tool-metadata-url and --tool-metadata-key." - ); + if (apiSource === "public-catalog") { + return createPublicCatalogToolkitSource(options, metadataSource, verbose); } + if (verbose) { - console.log( - chalk.dim(`Using /v1/tool_metadata endpoint: ${config.baseUrl}`) - ); + console.log(chalk.dim(`Using mock data: ${mockDataDir}`)); } - return createEngineToolkitDataSource({ - engine: config, - metadataSource, + return createMockToolkitDataSource({ + dataDir: mockDataDir, }); }; -const createToolkitDataSourceForApi = ( - apiSource: ApiSource, - options: ToolkitDataSourceOptions, - metadataSource: ReturnType, - mockDataDir: string, - verbose: boolean, - spinner?: ReturnType -): ToolkitDataSource => { - warnDeprecatedApiSource(apiSource); - - switch (apiSource) { - case "public-catalog": - return createPublicCatalogToolkitSource(options, metadataSource, verbose); - case "list-tools": - return createListToolsToolkitSource( - options, - metadataSource, - verbose, - spinner - ); - case "tool-metadata": - return createToolMetadataToolkitSource(options, metadataSource, verbose); - default: - if (verbose) { - console.log(chalk.dim(`Using mock data: ${mockDataDir}`)); - } - return createMockToolkitDataSource({ - dataDir: mockDataDir, - }); - } -}; - const normalizeToolkitKey = (toolkitId: string): string => toolkitId.toLowerCase(); @@ -911,32 +793,15 @@ program .option("--metadata-file ", "Path to metadata JSON file") .option( "--api-source ", - 'API source: "public-catalog" (/v1/public/*), "list-tools" (/v1/tools), "tool-metadata" (/v1/tool_metadata, deprecated), or "mock" (default: auto-detect)' - ) - .option( - "--list-tools-url ", - "List tools API URL (default: https://api.arcade.dev)" - ) - .option( - "--list-tools-key ", - "List tools API key (or ARCADE_API_KEY env)" - ) - .option( - "--list-tools-page-size ", - "List tools API page size", - (value) => Number.parseInt(value, 10) + 'API source: "public-catalog" (/v1/public/*) or "mock" (default: auto-detect)' ) .option( - "--tool-metadata-url ", - "Engine API base URL (or ENGINE_API_URL env)" + "--api-url ", + "Arcade API base URL (or ARCADE_API_URL / ENGINE_API_URL env)" ) .option( - "--tool-metadata-key ", - "Tool metadata API key (or ENGINE_API_KEY env; deprecated, only for tool-metadata source)" - ) - .option( - "--tool-metadata-page-size ", - "Tool metadata API page size", + "--api-page-size ", + "Public catalog tools page size", (value) => Number.parseInt(value, 10) ) .option("--previous-output ", "Path to previous output directory") @@ -1059,12 +924,8 @@ program mockDataDir?: string; metadataFile?: string; apiSource?: string; - listToolsUrl?: string; - listToolsKey?: string; - listToolsPageSize?: number; - toolMetadataUrl?: string; - toolMetadataKey?: string; - toolMetadataPageSize?: number; + apiUrl?: string; + apiPageSize?: number; previousOutput?: string; forceRegenerate: boolean; overwriteOutput?: boolean; @@ -1255,8 +1116,7 @@ program options, metadataSource, mockDataDir, - options.verbose, - spinner + options.verbose ) ); @@ -2037,32 +1897,15 @@ program .option("--metadata-file ", "Path to metadata JSON file") .option( "--api-source ", - 'API source: "public-catalog" (/v1/public/*), "list-tools" (/v1/tools), "tool-metadata" (/v1/tool_metadata, deprecated), or "mock" (default: auto-detect)' - ) - .option( - "--list-tools-url ", - "List tools API URL (default: https://api.arcade.dev)" + 'API source: "public-catalog" (/v1/public/*) or "mock" (default: auto-detect)' ) .option( - "--list-tools-key ", - "List tools API key (or ARCADE_API_KEY env)" + "--api-url ", + "Arcade API base URL (or ARCADE_API_URL / ENGINE_API_URL env)" ) .option( - "--list-tools-page-size ", - "List tools API page size", - (value) => Number.parseInt(value, 10) - ) - .option( - "--tool-metadata-url ", - "Engine API base URL (or ENGINE_API_URL env)" - ) - .option( - "--tool-metadata-key ", - "Tool metadata API key (or ENGINE_API_KEY env; deprecated, only for tool-metadata source)" - ) - .option( - "--tool-metadata-page-size ", - "Tool metadata API page size", + "--api-page-size ", + "Public catalog tools page size", (value) => Number.parseInt(value, 10) ) .option("--previous-output ", "Path to previous output directory") @@ -2171,12 +2014,8 @@ program mockDataDir?: string; metadataFile?: string; apiSource?: string; - listToolsUrl?: string; - listToolsKey?: string; - listToolsPageSize?: number; - toolMetadataUrl?: string; - toolMetadataKey?: string; - toolMetadataPageSize?: number; + apiUrl?: string; + apiPageSize?: number; previousOutput?: string; forceRegenerate: boolean; overwriteOutput?: boolean; @@ -2264,8 +2103,7 @@ program options, metadataSource, mockDataDir, - options.verbose, - spinner + options.verbose ) ); @@ -2888,29 +2726,17 @@ program .option("--metadata-file ", "Path to metadata JSON file") .option( "--api-source ", - 'API source: "public-catalog" (/v1/public/*), "list-tools" (/v1/tools), "tool-metadata" (/v1/tool_metadata, deprecated), or "mock" (default: auto-detect)' + 'API source: "public-catalog" (/v1/public/*) or "mock" (default: auto-detect)' ) .option( - "--list-tools-url ", - "List tools API URL (default: https://api.arcade.dev)" + "--api-url ", + "Arcade API base URL (or ARCADE_API_URL / ENGINE_API_URL env)" ) .option( - "--list-tools-key ", - "List tools API key (or ARCADE_API_KEY env)" - ) - .option( - "--list-tools-page-size ", - "List tools API page size", + "--api-page-size ", + "Public catalog tools page size", (value) => Number.parseInt(value, 10) ) - .option( - "--tool-metadata-url ", - "Engine API base URL (or ENGINE_API_URL env)" - ) - .option( - "--tool-metadata-key ", - "Tool metadata API key (or ENGINE_API_KEY env; deprecated, only for tool-metadata source)" - ) .option( "--custom-sections ", "Path to the authoritative Markdown/MDX curation directory (defaults to ./curation when present)" @@ -2924,11 +2750,8 @@ program mockDataDir?: string; metadataFile?: string; apiSource?: string; - listToolsUrl?: string; - listToolsKey?: string; - listToolsPageSize?: number; - toolMetadataUrl?: string; - toolMetadataKey?: string; + apiUrl?: string; + apiPageSize?: number; customSections?: string; verbose: boolean; json: boolean; @@ -2955,8 +2778,7 @@ program options, metadataSource, mockDataDir, - false, // not verbose during fetch - spinner + false // not verbose during fetch ) ); diff --git a/toolkit-docs-generator/src/llm/secret-edit-generator.ts b/toolkit-docs-generator/src/llm/secret-edit-generator.ts index 7659ad61e..a10f1ee4d 100644 --- a/toolkit-docs-generator/src/llm/secret-edit-generator.ts +++ b/toolkit-docs-generator/src/llm/secret-edit-generator.ts @@ -56,9 +56,6 @@ export interface SecretEditGenerator { fillCoverageGaps: (input: SecretCoverageEditInput) => Promise; } -/** @deprecated Use {@link SecretEditGenerator} */ -export type ISecretEditGenerator = SecretEditGenerator; - const DEFAULT_SYSTEM_PROMPT = "You are a careful documentation editor for the Arcade MCP toolkit docs. " + "You make the smallest possible change that satisfies the request. " + diff --git a/toolkit-docs-generator/src/sources/arcade-api-types.ts b/toolkit-docs-generator/src/sources/arcade-api-types.ts deleted file mode 100644 index 6d56c56f4..000000000 --- a/toolkit-docs-generator/src/sources/arcade-api-types.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Type definitions for the Arcade Production API (/v1/tools endpoint) - * - * This API has a different response schema from the Engine API's /v1/tool_metadata endpoint. - */ - -import { z } from "zod"; - -// ============================================================================ -// Arcade API Response Types -// ============================================================================ - -/** - * Value schema for parameters - */ -export const ArcadeValueSchemaSchema = z.object({ - val_type: z.string(), - inner_val_type: z.string().nullish(), - enum: z.array(z.string()).nullish(), -}); - -export type ArcadeValueSchema = z.infer; - -/** - * Tool parameter from Arcade API - */ -export const ArcadeParameterSchema = z.object({ - name: z.string(), - required: z.boolean(), - description: z.string().nullish(), - value_schema: ArcadeValueSchemaSchema.nullish(), - inferrable: z.boolean().nullish(), -}); - -export type ArcadeParameter = z.infer; - -/** - * Tool input from Arcade API - */ -export const ArcadeInputSchema = z.object({ - parameters: z.array(ArcadeParameterSchema).nullish(), -}); - -export type ArcadeInput = z.infer; - -/** - * Tool output from Arcade API - */ -export const ArcadeOutputSchema = z.object({ - available_modes: z.array(z.string()).nullish(), - description: z.string().nullish(), - value_schema: ArcadeValueSchemaSchema.nullish(), -}); - -export type ArcadeOutput = z.infer; - -/** - * OAuth2 requirements - */ -export const ArcadeOAuth2Schema = z.object({ - scopes: z.array(z.string()).nullish(), -}); - -export type ArcadeOAuth2 = z.infer; - -/** - * Authorization requirements - */ -export const ArcadeAuthorizationSchema = z.object({ - id: z.string().nullish(), - provider_id: z.string().nullish(), - provider_type: z.string().nullish(), - oauth2: ArcadeOAuth2Schema.nullish(), - status: z.string().nullish(), - status_reason: z.string().nullish(), - token_status: z.string().nullish(), -}); - -export type ArcadeAuthorization = z.infer; - -/** - * Secret requirement - */ -export const ArcadeSecretSchema = z.object({ - key: z.string(), - met: z.boolean().nullish(), - status_reason: z.string().nullish(), -}); - -export type ArcadeSecret = z.infer; - -/** - * Tool requirements - */ -export const ArcadeRequirementsSchema = z.object({ - met: z.boolean().nullish(), - authorization: ArcadeAuthorizationSchema.nullish(), - secrets: z.array(ArcadeSecretSchema).nullish(), -}); - -export type ArcadeRequirements = z.infer; - -/** - * Toolkit info embedded in tool response - */ -export const ArcadeToolkitInfoSchema = z.object({ - name: z.string(), - description: z.string().nullish(), - version: z.string().nullish(), -}); - -export type ArcadeToolkitInfo = z.infer; - -/** - * Single tool from Arcade API - */ -export const ArcadeToolSchema = z.object({ - fully_qualified_name: z.string(), - qualified_name: z.string(), - name: z.string(), - description: z.string().nullish(), - toolkit: ArcadeToolkitInfoSchema, - input: ArcadeInputSchema.nullish(), - output: ArcadeOutputSchema.nullish(), - requirements: ArcadeRequirementsSchema.nullish(), -}); - -export type ArcadeTool = z.infer; - -/** - * Paginated response from /v1/tools - */ -export const ArcadeToolsResponseSchema = z.object({ - items: z.array(ArcadeToolSchema), - limit: z.number(), - offset: z.number(), - page_count: z.number().optional(), - total_count: z.number(), -}); - -export type ArcadeToolsResponse = z.infer; - -/** - * Error response from Arcade API - */ -export const ArcadeErrorResponseSchema = z.object({ - detail: z.string().optional(), - message: z.string().optional(), - error: z.string().optional(), -}); - -export type ArcadeErrorResponse = z.infer; - -// ============================================================================ -// Parse Functions -// ============================================================================ - -/** - * Parse Arcade API tools response - */ -export const parseArcadeToolsResponse = ( - payload: unknown -): ArcadeToolsResponse => { - const result = ArcadeToolsResponseSchema.safeParse(payload); - if (!result.success) { - throw new Error(`Invalid Arcade API response: ${result.error.message}`); - } - return result.data; -}; - -/** - * Parse Arcade API error response - */ -export const parseArcadeErrorResponse = ( - payload: unknown -): ArcadeErrorResponse | null => { - const result = ArcadeErrorResponseSchema.safeParse(payload); - if (!result.success) { - return null; - } - return result.data; -}; diff --git a/toolkit-docs-generator/src/sources/arcade-api.ts b/toolkit-docs-generator/src/sources/arcade-api.ts deleted file mode 100644 index 3f03020b3..000000000 --- a/toolkit-docs-generator/src/sources/arcade-api.ts +++ /dev/null @@ -1,344 +0,0 @@ -/** - * Arcade Production API Source - * - * Fetches tool data from the Arcade production API endpoint: /v1/tools - * This endpoint has a different schema from the Engine API's /v1/tool_metadata. - */ - -import type { - ToolAuth, - ToolDefinition, - ToolOutput, - ToolParameter, -} from "../types/index"; -import { - type ArcadeTool, - parseArcadeErrorResponse, - parseArcadeToolsResponse, -} from "./arcade-api-types"; -import type { FetchOptions, ToolDataSource } from "./internal"; - -// ============================================================================ -// Configuration -// ============================================================================ - -export interface ArcadeApiSourceConfig { - /** Base URL for Arcade API (e.g., https://api.arcade.dev) */ - readonly baseUrl: string; - /** Arcade API key (Bearer token) */ - readonly apiKey: string; - /** Optional fetch implementation for testing */ - readonly fetchFn?: typeof fetch; - /** Page size for pagination (default: 100, max: 100) */ - readonly pageSize?: number; - /** Optional progress callback for pagination */ - readonly onProgress?: ((fetched: number, total: number) => void) | undefined; -} - -// ============================================================================ -// Constants -// ============================================================================ - -const DEFAULT_PAGE_SIZE = 1000; -const MAX_PAGE_SIZE = 1000; -const DEFAULT_BASE_URL = "https://api.arcade.dev"; - -// ============================================================================ -// Utility Functions -// ============================================================================ - -const normalizePageSize = (value?: number): number => { - if (!value || Number.isNaN(value) || value <= 0) { - return DEFAULT_PAGE_SIZE; - } - return Math.min(value, MAX_PAGE_SIZE); -}; - -const normalizeBaseUrl = (baseUrl: string): string => - baseUrl.replace(/\/+$/, ""); - -const buildEndpointUrl = (baseUrl: string): string => { - const normalized = normalizeBaseUrl(baseUrl); - if (normalized.endsWith("/v1")) { - return `${normalized}/tools`; - } - return `${normalized}/v1/tools`; -}; - -// ============================================================================ -// Response Transformation -// ============================================================================ - -/** - * Map Arcade API value type to internal type string - */ -const mapValueType = (valType: string | undefined): string => { - if (!valType) return "string"; - - const typeMap: Record = { - string: "string", - integer: "integer", - number: "number", - boolean: "boolean", - array: "array", - object: "object", - json: "json", - }; - - return typeMap[valType.toLowerCase()] ?? valType; -}; - -/** - * Transform Arcade API tool to internal ToolDefinition format - */ -const transformArcadeTool = (arcadeTool: ArcadeTool): ToolDefinition => { - // Transform parameters - handle null/undefined parameters array - const rawParams = arcadeTool.input?.parameters; - const parameters: ToolParameter[] = (rawParams ?? []).map((param) => ({ - name: param.name, - type: mapValueType(param.value_schema?.val_type), - innerType: param.value_schema?.inner_val_type ?? undefined, - required: param.required, - description: param.description ?? null, - enum: param.value_schema?.enum ?? null, - inferrable: param.inferrable ?? true, - })); - - // Transform auth - handle null/undefined authorization - let auth: ToolAuth | null = null; - const authReq = arcadeTool.requirements?.authorization; - if (authReq?.provider_type) { - auth = { - providerId: authReq.provider_id ?? null, - providerType: authReq.provider_type, - scopes: authReq.oauth2?.scopes ?? [], - }; - } - - // Transform secrets - handle null/undefined secrets array - const rawSecrets = arcadeTool.requirements?.secrets; - const secrets: string[] = (rawSecrets ?? []).map((s) => s.key); - - // Transform output - handle null/undefined output - let output: ToolOutput | null = null; - if (arcadeTool.output) { - output = { - type: mapValueType(arcadeTool.output.value_schema?.val_type), - description: arcadeTool.output.description ?? null, - }; - } - - return { - name: arcadeTool.name, - qualifiedName: arcadeTool.qualified_name, - fullyQualifiedName: arcadeTool.fully_qualified_name, - description: arcadeTool.description ?? null, - toolkitDescription: arcadeTool.toolkit.description ?? null, - parameters, - auth, - secrets, - output, - }; -}; - -/** - * Extract toolkit ID from qualified name (e.g., "GoogleCalendar.CreateEvent" -> "GoogleCalendar") - */ -const extractToolkitId = (qualifiedName: string): string => { - const parts = qualifiedName.split("."); - return parts[0] ?? qualifiedName; -}; - -// ============================================================================ -// Arcade API Source Implementation -// ============================================================================ - -/** @deprecated Use the public catalog source ({@link createPublicCatalogApiSource}) instead. */ -export class ArcadeApiSource implements ToolDataSource { - private readonly endpoint: string; - private readonly apiKey: string; - private readonly fetchFn: typeof fetch; - private readonly pageSize: number; - private readonly onProgress: - | ((fetched: number, total: number) => void) - | undefined; - - constructor(config: ArcadeApiSourceConfig) { - this.endpoint = buildEndpointUrl(config.baseUrl); - this.apiKey = config.apiKey; - this.fetchFn = config.fetchFn ?? fetch; - this.pageSize = normalizePageSize(config.pageSize); - this.onProgress = config.onProgress; - } - - /** - * Fetch a single page of tools from the API - */ - private async fetchPage( - _options: FetchOptions | undefined, - offset: number - ): Promise<{ items: ToolDefinition[]; totalCount: number }> { - const url = new URL(this.endpoint); - url.searchParams.set("limit", String(this.pageSize)); - url.searchParams.set("offset", String(offset)); - - // Note: The Arcade API /v1/tools endpoint does not support server-side toolkit filtering. - // All filtering is done client-side in fetchAllTools(). - - const response = await this.fetchFn(url.toString(), { - headers: { - Authorization: `Bearer ${this.apiKey}`, - Accept: "application/json", - }, - }); - - if (!response.ok) { - const errorDetail = await this.getErrorDetail(response); - throw new Error( - `Arcade API error ${response.status}: ${errorDetail ?? response.statusText}` - ); - } - - const payload = (await response.json()) as unknown; - const parsed = parseArcadeToolsResponse(payload); - - // Transform Arcade tools to internal format - const items = parsed.items.map(transformArcadeTool); - - return { - items, - totalCount: parsed.total_count, - }; - } - - /** - * Fetch all tools, handling pagination - */ - async fetchAllTools( - options?: FetchOptions - ): Promise { - const allTools: ToolDefinition[] = []; - let offset = 0; - let totalCount = 0; - - while (true) { - const page = await this.fetchPage(options, offset); - - if (offset === 0) { - totalCount = page.totalCount; - this.onProgress?.(0, totalCount); - } - - allTools.push(...page.items); - - // Report progress - this.onProgress?.(allTools.length, totalCount); - - if (page.items.length === 0) { - break; - } - - offset += page.items.length; - - if (offset >= totalCount) { - break; - } - } - - // Client-side filtering by toolkit if specified - if (options?.toolkitId) { - const toolkitIdLower = options.toolkitId.toLowerCase(); - return allTools.filter((tool) => { - const toolToolkitId = extractToolkitId(tool.qualifiedName); - return toolToolkitId.toLowerCase() === toolkitIdLower; - }); - } - - // Client-side filtering by version if specified - if (options?.version) { - return allTools.filter((tool) => { - // fullyQualifiedName format: "Toolkit.Tool@version" - return tool.fullyQualifiedName.endsWith(`@${options.version}`); - }); - } - - return allTools; - } - - /** - * Fetch tools for a specific toolkit - */ - async fetchToolsByToolkit( - toolkitId: string - ): Promise { - return this.fetchAllTools({ toolkitId }); - } - - /** - * Check if the API is available - */ - async isAvailable(): Promise { - try { - const url = new URL(this.endpoint); - url.searchParams.set("limit", "1"); - url.searchParams.set("offset", "0"); - - const response = await this.fetchFn(url.toString(), { - headers: { - Authorization: `Bearer ${this.apiKey}`, - Accept: "application/json", - }, - }); - - return response.ok; - } catch { - return false; - } - } - - /** - * Extract error detail from API response - */ - private async getErrorDetail(response: Response): Promise { - const contentType = response.headers.get("content-type") ?? ""; - if (!contentType.includes("application/json")) { - return null; - } - - try { - const payload = (await response.json()) as unknown; - const parsed = parseArcadeErrorResponse(payload); - if (parsed) { - return parsed.detail ?? parsed.message ?? parsed.error ?? null; - } - } catch { - return null; - } - - return null; - } -} - -// ============================================================================ -// Factory Function -// ============================================================================ - -/** - * Create an Arcade API source - */ -export const createArcadeApiSource = ( - config: ArcadeApiSourceConfig -): ToolDataSource => new ArcadeApiSource(config); - -/** - * Create an Arcade API source with default production URL - */ -export const createProductionArcadeApiSource = ( - apiKey: string, - options?: Partial> -): ToolDataSource => - new ArcadeApiSource({ - baseUrl: DEFAULT_BASE_URL, - apiKey, - ...options, - }); diff --git a/toolkit-docs-generator/src/sources/engine-api.ts b/toolkit-docs-generator/src/sources/engine-api.ts deleted file mode 100644 index 3cb838660..000000000 --- a/toolkit-docs-generator/src/sources/engine-api.ts +++ /dev/null @@ -1,220 +0,0 @@ -import type { ToolDefinition } from "../types/index"; -import type { FetchOptions, ToolDataSource } from "./internal"; -import { - parseToolMetadataError, - parseToolMetadataResponse, - parseToolMetadataSummaryResponse, - type ToolMetadataSummary, -} from "./tool-metadata-schema"; - -export interface EngineApiSourceConfig { - /** Base URL for Engine (e.g., https://api.arcade.dev) */ - readonly baseUrl: string; - /** Engine API key (Bearer token) */ - readonly apiKey: string; - /** Optional fetch implementation for testing */ - readonly fetchFn?: typeof fetch; - /** Page size for pagination */ - readonly pageSize?: number; - /** Include all versions in results (required for version filtering) */ - readonly includeAllVersions?: boolean; -} - -type ToolMetadataPage = { - items: ToolDefinition[]; - totalCount: number; -}; - -const DEFAULT_PAGE_SIZE = 1000; -const MAX_PAGE_SIZE = 1000; - -const normalizePageSize = (value?: number): number => { - if (!value || Number.isNaN(value) || value <= 0) { - return DEFAULT_PAGE_SIZE; - } - return Math.min(value, MAX_PAGE_SIZE); -}; - -const normalizeBaseUrl = (baseUrl: string): string => - baseUrl.replace(/\/+$/, ""); - -const buildEndpointUrl = (baseUrl: string, path: string): string => { - const normalized = normalizeBaseUrl(baseUrl); - if (normalized.endsWith("/v1")) { - return `${normalized}/${path}`; - } - return `${normalized}/v1/${path}`; -}; - -const parseJsonResponse = async ( - response: Response, - context: string -): Promise => { - try { - return await response.json(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error( - `Engine API returned invalid JSON for ${context}: ${message}` - ); - } -}; - -/** @deprecated Use the public catalog source ({@link createPublicCatalogApiSource}) instead. */ -export class EngineApiSource implements ToolDataSource { - private readonly endpoint: string; - private readonly summaryEndpoint: string; - private readonly apiKey: string; - private readonly fetchFn: typeof fetch; - private readonly pageSize: number; - private readonly includeAllVersions: boolean; - - constructor(config: EngineApiSourceConfig) { - this.endpoint = buildEndpointUrl(config.baseUrl, "tool_metadata"); - this.summaryEndpoint = buildEndpointUrl( - config.baseUrl, - "tool_metadata_summary" - ); - this.apiKey = config.apiKey; - this.fetchFn = config.fetchFn ?? fetch; - this.pageSize = normalizePageSize(config.pageSize); - this.includeAllVersions = config.includeAllVersions ?? false; - } - - private async fetchPage( - options: FetchOptions | undefined, - offset: number - ): Promise { - const url = new URL(this.endpoint); - url.searchParams.set("limit", String(this.pageSize)); - url.searchParams.set("offset", String(offset)); - // latest_only defaults to true on the server; set to false when we need all versions - const includeAllVersions = options?.version - ? true - : this.includeAllVersions; - if (includeAllVersions) { - url.searchParams.set("latest_only", "false"); - } - - if (options?.toolkitId) { - url.searchParams.set("toolkit", options.toolkitId); - } - if (options?.version) { - url.searchParams.set("version", options.version); - } - if (options?.providerId) { - url.searchParams.set("auth_provider", options.providerId); - } - - const response = await this.fetchFn(url.toString(), { - headers: { - Authorization: `Bearer ${this.apiKey}`, - Accept: "application/json", - }, - }); - - if (!response.ok) { - const errorDetail = await this.getErrorDetail(response); - throw new Error( - `Engine API error ${response.status}: ${errorDetail ?? response.statusText}` - ); - } - - const payload = await parseJsonResponse(response, "tool metadata"); - const parsed = parseToolMetadataResponse(payload); - return { - items: parsed.items, - totalCount: parsed.totalCount, - }; - } - - private async fetchSummary(): Promise { - const url = new URL(this.summaryEndpoint); - - const response = await this.fetchFn(url.toString(), { - headers: { - Authorization: `Bearer ${this.apiKey}`, - Accept: "application/json", - }, - }); - - if (!response.ok) { - const errorDetail = await this.getErrorDetail(response); - throw new Error( - `Engine API error ${response.status}: ${errorDetail ?? response.statusText}` - ); - } - - const payload = await parseJsonResponse(response, "tool metadata summary"); - return parseToolMetadataSummaryResponse(payload); - } - - async fetchToolsByToolkit( - toolkitId: string - ): Promise { - return this.fetchAllTools({ toolkitId }); - } - - async fetchAllTools( - options?: FetchOptions - ): Promise { - const tools: ToolDefinition[] = []; - let offset = 0; - let totalCount = 0; - - while (true) { - const page = await this.fetchPage(options, offset); - if (offset === 0) { - totalCount = page.totalCount; - } - tools.push(...page.items); - - if (page.items.length === 0) { - break; - } - - offset += page.items.length; - if (offset >= totalCount) { - break; - } - } - - return tools; - } - - async fetchToolkitsSummary(): Promise { - return this.fetchSummary(); - } - - async isAvailable(): Promise { - try { - await this.fetchPage(undefined, 0); - return true; - } catch { - return false; - } - } - - private async getErrorDetail(response: Response): Promise { - const contentType = response.headers.get("content-type") ?? ""; - if (!contentType.includes("application/json")) { - return null; - } - - try { - const payload = (await response.json()) as unknown; - const parsed = parseToolMetadataError(payload); - if (parsed) { - return `${parsed.name}: ${parsed.message}`; - } - } catch { - return null; - } - - return null; - } -} - -export const createEngineApiSource = ( - config: EngineApiSourceConfig -): ToolDataSource => new EngineApiSource(config); diff --git a/toolkit-docs-generator/src/sources/in-memory.ts b/toolkit-docs-generator/src/sources/in-memory.ts index 658636c18..d829c5f43 100644 --- a/toolkit-docs-generator/src/sources/in-memory.ts +++ b/toolkit-docs-generator/src/sources/in-memory.ts @@ -19,7 +19,7 @@ import type { FetchOptions, MetadataSource, ToolDataSource } from "./internal"; // ============================================================================ /** - * In-memory implementation of IToolDataSource for testing + * In-memory implementation of ToolDataSource for testing * * Use this instead of mocking the interface. Simply provide * realistic test data in the constructor. @@ -89,7 +89,7 @@ export class InMemoryToolDataSource implements ToolDataSource { // ============================================================================ /** - * In-memory implementation of IMetadataSource for testing + * In-memory implementation of MetadataSource for testing * * @example * ```typescript diff --git a/toolkit-docs-generator/src/sources/index.ts b/toolkit-docs-generator/src/sources/index.ts index 9785949f4..bcc77e45f 100644 --- a/toolkit-docs-generator/src/sources/index.ts +++ b/toolkit-docs-generator/src/sources/index.ts @@ -2,15 +2,12 @@ * Data sources exports */ -export * from "./arcade-api"; -export * from "./arcade-api-types"; export * from "./design-system-metadata"; -export * from "./engine-api"; export * from "./in-memory"; export * from "./interfaces"; export * from "./markdown-curation"; -export * from "./mock-engine-api"; export * from "./mock-metadata"; +export * from "./mock-tool-fixture-source"; export * from "./oauth-provider-resolver"; export * from "./public-catalog-api"; export * from "./public-catalog-pagination"; diff --git a/toolkit-docs-generator/src/sources/internal.ts b/toolkit-docs-generator/src/sources/internal.ts index fbb836fc2..7f2aad034 100644 --- a/toolkit-docs-generator/src/sources/internal.ts +++ b/toolkit-docs-generator/src/sources/internal.ts @@ -36,9 +36,6 @@ export interface ToolDataSource { readonly isAvailable: () => Promise; } -/** @deprecated Use {@link ToolDataSource} */ -export type IToolDataSource = ToolDataSource; - // ============================================================================ // Metadata Source Interface (internal) // ============================================================================ @@ -53,6 +50,3 @@ export interface MetadataSource { /** List all available toolkit IDs */ readonly listToolkitIds: () => Promise; } - -/** @deprecated Use {@link MetadataSource} */ -export type IMetadataSource = MetadataSource; diff --git a/toolkit-docs-generator/src/sources/mock-metadata.ts b/toolkit-docs-generator/src/sources/mock-metadata.ts index 6e7b07907..223e2ca5b 100644 --- a/toolkit-docs-generator/src/sources/mock-metadata.ts +++ b/toolkit-docs-generator/src/sources/mock-metadata.ts @@ -30,7 +30,7 @@ export interface MockMetadataConfig { } /** - * Mock implementation of IMetadataSource that loads from JSON fixtures + * Mock implementation of MetadataSource that loads from JSON fixtures */ export class MockMetadataSource implements MetadataSource { private readonly fixtureFilePath: string; diff --git a/toolkit-docs-generator/src/sources/mock-engine-api.ts b/toolkit-docs-generator/src/sources/mock-tool-fixture-source.ts similarity index 72% rename from toolkit-docs-generator/src/sources/mock-engine-api.ts rename to toolkit-docs-generator/src/sources/mock-tool-fixture-source.ts index dfe234e83..d61d10b34 100644 --- a/toolkit-docs-generator/src/sources/mock-engine-api.ts +++ b/toolkit-docs-generator/src/sources/mock-tool-fixture-source.ts @@ -1,9 +1,7 @@ /** - * Mock Engine API Source + * Mock tool fixture source * - * This source loads tool definitions from JSON fixtures, simulating - * what the real Engine API will return. Replace with EngineApiSource - * when the API endpoint is ready. + * Loads tool definitions from JSON fixtures for local development and tests. */ import { readFile } from "fs/promises"; import type { ToolDefinition } from "../types/index"; @@ -11,22 +9,16 @@ import { normalizeId } from "../utils/fp"; import type { FetchOptions, ToolDataSource } from "./internal"; import { parseToolMetadataResponse } from "./tool-metadata-schema"; -export interface MockEngineApiConfig { +export interface MockToolFixtureSourceConfig { /** Path to the JSON fixture file */ fixtureFilePath: string; } -/** - * Mock implementation of IToolDataSource that loads from JSON fixtures - * - * Use this until the real Engine API endpoint is available. - * The fixture format matches the expected API response schema. - */ -export class MockEngineApiSource implements ToolDataSource { +export class MockToolFixtureSource implements ToolDataSource { private readonly fixtureFilePath: string; private cachedData: ToolDefinition[] | null = null; - constructor(config: MockEngineApiConfig) { + constructor(config: MockToolFixtureSourceConfig) { this.fixtureFilePath = config.fixtureFilePath; } @@ -93,10 +85,6 @@ export class MockEngineApiSource implements ToolDataSource { } } -// ============================================================================ -// Factory -// ============================================================================ - -export const createMockEngineApiSource = ( +export const createMockToolFixtureSource = ( fixtureFilePath: string -): ToolDataSource => new MockEngineApiSource({ fixtureFilePath }); +): ToolDataSource => new MockToolFixtureSource({ fixtureFilePath }); diff --git a/toolkit-docs-generator/src/sources/toolkit-data-source.ts b/toolkit-docs-generator/src/sources/toolkit-data-source.ts index 64a0b1644..9374ef39b 100644 --- a/toolkit-docs-generator/src/sources/toolkit-data-source.ts +++ b/toolkit-docs-generator/src/sources/toolkit-data-source.ts @@ -2,25 +2,15 @@ * Unified Toolkit Data Source * * This abstraction combines tool definitions and metadata into a single interface. - * This allows us to swap implementations easily when the data comes from a single source - * in the future (e.g., when Engine API includes metadata). */ import { join } from "path"; import { isApiSuffixedToolkitId } from "../shared/toolkit-primitives"; import type { ToolDefinition, ToolkitMetadata } from "../types/index"; import { filterToolsByHighestVersion } from "../utils/version-coherence"; -import { - type ArcadeApiSourceConfig, - createArcadeApiSource, -} from "./arcade-api"; -import { - createEngineApiSource, - type EngineApiSourceConfig, -} from "./engine-api"; import type { MetadataSource, ToolDataSource } from "./internal"; -import { createMockEngineApiSource } from "./mock-engine-api"; import { createMockMetadataSource } from "./mock-metadata"; +import { createMockToolFixtureSource } from "./mock-tool-fixture-source"; import { createPublicCatalogApiSource, type PublicCatalogApiSourceConfig, @@ -34,7 +24,7 @@ import { * Combined toolkit data containing both tools and metadata */ export interface ToolkitData { - /** Tool definitions from Engine API */ + /** Tool definitions from the public catalog API */ readonly tools: readonly ToolDefinition[]; /** Metadata from Design System */ readonly metadata: ToolkitMetadata | null; @@ -46,47 +36,22 @@ export interface ToolkitData { /** * Interface for fetching combined toolkit data (tools + metadata) - * - * This abstraction allows us to: - * 1. Currently combine separate Engine API and Design System sources - * 2. Future: Use a single unified source when Engine API includes metadata - * - * Implementations: - * - CombinedToolkitDataSource: Combines ToolDataSource + MetadataSource - * - UnifiedToolkitDataSource: Single source (future implementation) */ export interface ToolkitDataSource { - /** - * Fetch combined data for a specific toolkit - * @param toolkitId - The toolkit identifier (e.g., "Github", "Slack") - * @param version - Optional version filter - */ readonly fetchToolkitData: ( toolkitId: string, version?: string ) => Promise; - /** - * Fetch combined data for all toolkits - */ readonly fetchAllToolkitsData: () => Promise< ReadonlyMap >; - /** - * Check if the data source is available - */ readonly isAvailable: () => Promise; } -/** @deprecated Use {@link ToolkitDataSource} */ -export type IToolkitDataSource = ToolkitDataSource; - /** * Reuse one all-toolkit snapshot for the lifetime of a generation run. - * - * Change detection, progress setup, and merging all consume the same data - * instead of issuing independent API reads that can disagree mid-run. */ export const createCachedToolkitDataSource = ( source: ToolkitDataSource @@ -107,29 +72,14 @@ export const createCachedToolkitDataSource = ( }; // ============================================================================ -// Combined Implementation (Current: Separate Sources) +// Combined Implementation // ============================================================================ -/** - * Configuration for combined toolkit data source - */ export interface CombinedToolkitDataSourceConfig { - /** Source for tool definitions */ readonly toolSource: ToolDataSource; - /** Source for toolkit metadata */ readonly metadataSource: MetadataSource; } -/** - * Combined implementation that merges separate tool and metadata sources - * - * This is the current implementation that combines: - * - Engine API (via ToolDataSource) - * - Design System (via MetadataSource) - * - * In the future, this can be replaced with UnifiedToolkitDataSource - * when Engine API includes metadata. - */ export class CombinedToolkitDataSource implements ToolkitDataSource { private readonly toolSource: ToolDataSource; private readonly metadataSource: MetadataSource; @@ -139,11 +89,6 @@ export class CombinedToolkitDataSource implements ToolkitDataSource { this.metadataSource = config.metadataSource; } - /** - * Apply the "*Api" provider-id fallback when the direct metadata lookup - * missed. Used by both `fetchToolkitData` and `fetchAllToolkitsData` so - * they resolve metadata the same way. - */ private async resolveProviderMetadata( toolkitId: string, tools: readonly ToolDefinition[], @@ -168,7 +113,6 @@ export class CombinedToolkitDataSource implements ToolkitDataSource { toolkitId: string, version?: string ): Promise { - // Fetch tools and metadata in parallel const [tools, directMetadata] = await Promise.all([ this.toolSource.fetchToolsByToolkit(toolkitId), this.metadataSource.getToolkitMetadata(toolkitId), @@ -179,8 +123,6 @@ export class CombinedToolkitDataSource implements ToolkitDataSource { directMetadata ); - // Filter tools by version if specified, otherwise keep only the highest - // version to drop stale tools from older releases that Engine still serves. const filteredTools = version ? tools.filter((tool) => { const toolVersion = tool.fullyQualifiedName.split("@")[1]; @@ -195,13 +137,11 @@ export class CombinedToolkitDataSource implements ToolkitDataSource { } async fetchAllToolkitsData(): Promise> { - // Fetch all tools and metadata in parallel const [allTools, allMetadata] = await Promise.all([ this.toolSource.fetchAllTools(), this.metadataSource.getAllToolkitsMetadata(), ]); - // Group tools by toolkit ID const toolkitGroups = new Map(); for (const tool of allTools) { const toolkitId = tool.qualifiedName.split(".")[0]; @@ -211,14 +151,11 @@ export class CombinedToolkitDataSource implements ToolkitDataSource { } } - // Create metadata lookup map const metadataMap = new Map(); for (const metadata of allMetadata) { metadataMap.set(metadata.id, metadata); } - // Filter each toolkit to its highest version to drop stale - // tools from older releases that Engine still serves. for (const [toolkitId, tools] of toolkitGroups) { const filtered = filterToolsByHighestVersion(tools); if (filtered !== tools) { @@ -226,14 +163,8 @@ export class CombinedToolkitDataSource implements ToolkitDataSource { } } - // Combine into ToolkitData map. - // Use getToolkitMetadata for toolkits without a direct match so that - // fallback logic (e.g. "WeaviateApi" → "Weaviate") is applied consistently, - // matching the behaviour of fetchToolkitData. const result = new Map(); for (const [toolkitId, tools] of toolkitGroups) { - // Prefer the batch lookup; only fall back to per-toolkit lookup - // (and then the provider-id fallback) when the map misses. const directMetadata = metadataMap.get(toolkitId) ?? (await this.metadataSource.getToolkitMetadata(toolkitId)); @@ -251,31 +182,18 @@ export class CombinedToolkitDataSource implements ToolkitDataSource { async isAvailable(): Promise { const [toolAvailable, metadataAvailable] = await Promise.all([ this.toolSource.isAvailable(), - Promise.resolve(true), // Metadata source is always available (returns null if not found) + Promise.resolve(true), ]); return toolAvailable && metadataAvailable; } } -// ============================================================================ -// Factory -// ============================================================================ - -/** - * Create a combined toolkit data source from separate sources - */ export const createCombinedToolkitDataSource = ( config: CombinedToolkitDataSourceConfig ): ToolkitDataSource => new CombinedToolkitDataSource(config); -// ============================================================================ -// Public Catalog Toolkit Data Source -// ============================================================================ - export interface PublicCatalogToolkitDataSourceConfig { - /** Public catalog API configuration */ readonly publicCatalog: PublicCatalogApiSourceConfig; - /** Source for toolkit metadata */ readonly metadataSource: MetadataSource; } @@ -287,64 +205,10 @@ export const createPublicCatalogToolkitDataSource = ( metadataSource: config.metadataSource, }); -// ============================================================================ -// Engine Toolkit Data Source -// ============================================================================ - -export interface EngineToolkitDataSourceConfig { - /** Engine API configuration */ - readonly engine: EngineApiSourceConfig; - /** Source for toolkit metadata */ - readonly metadataSource: MetadataSource; -} - -/** @deprecated Use {@link createPublicCatalogToolkitDataSource} instead. */ -export const createEngineToolkitDataSource = ( - config: EngineToolkitDataSourceConfig -): ToolkitDataSource => - createCombinedToolkitDataSource({ - toolSource: createEngineApiSource(config.engine), - metadataSource: config.metadataSource, - }); - -// ============================================================================ -// Arcade API Toolkit Data Source -// ============================================================================ - -export interface ArcadeToolkitDataSourceConfig { - /** Arcade API configuration */ - readonly arcade: ArcadeApiSourceConfig; - /** Source for toolkit metadata */ - readonly metadataSource: MetadataSource; -} - -/** @deprecated Use {@link createPublicCatalogToolkitDataSource} instead. */ -export const createArcadeToolkitDataSource = ( - config: ArcadeToolkitDataSourceConfig -): ToolkitDataSource => - createCombinedToolkitDataSource({ - toolSource: createArcadeApiSource(config.arcade), - metadataSource: config.metadataSource, - }); - -// ============================================================================ -// Mock Toolkit Data Source (Current: JSON fixtures) -// ============================================================================ - -/** - * Configuration for mock toolkit data source - */ export interface MockToolkitDataSourceConfig { - /** Directory containing mock data fixtures */ readonly dataDir: string; } -/** - * Create a mock toolkit data source using JSON fixtures. - * - * This hides the fact that tools and metadata come from separate sources, - * while keeping a single abstraction for the rest of the system. - */ export const createMockToolkitDataSource = ( config: MockToolkitDataSourceConfig ): ToolkitDataSource => { @@ -352,7 +216,7 @@ export const createMockToolkitDataSource = ( const metadataFixturePath = join(config.dataDir, "metadata.json"); return createCombinedToolkitDataSource({ - toolSource: createMockEngineApiSource(toolFixturePath), + toolSource: createMockToolFixtureSource(toolFixturePath), metadataSource: createMockMetadataSource(metadataFixturePath), }); }; diff --git a/toolkit-docs-generator/tests/cli/api-source.test.ts b/toolkit-docs-generator/tests/cli/api-source.test.ts index 664dbe382..7cb9da1fe 100644 --- a/toolkit-docs-generator/tests/cli/api-source.test.ts +++ b/toolkit-docs-generator/tests/cli/api-source.test.ts @@ -1,5 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { resolveApiSource } from "../../src/cli/api-source"; +import { + resolveApiBaseUrlFromEnv, + resolveApiSource, +} from "../../src/cli/api-source"; const ORIGINAL_ENV = { ...process.env }; @@ -18,8 +21,6 @@ describe("resolveApiSource", () => { // biome-ignore lint/performance/noDelete: Required to actually remove env vars delete process.env.ENGINE_API_URL; // biome-ignore lint/performance/noDelete: Required to actually remove env vars - delete process.env.ARCADE_API_KEY; - // biome-ignore lint/performance/noDelete: Required to actually remove env vars delete process.env.ARCADE_API_URL; }); @@ -34,37 +35,33 @@ describe("resolveApiSource", () => { expect(resolveApiSource({ apiSource: "public" })).toBe("public-catalog"); }); - it("returns list-tools only when explicitly requested", () => { - expect(resolveApiSource({ apiSource: "list-tools" })).toBe("list-tools"); - }); - - it("accepts engine as an alias for tool-metadata", () => { - expect(resolveApiSource({ apiSource: "engine" })).toBe("tool-metadata"); + it("returns mock when explicitly requested", () => { + expect(resolveApiSource({ apiSource: "mock" })).toBe("mock"); }); - it("rejects unsupported aliases", () => { - expect(() => resolveApiSource({ apiSource: "arcade" })).toThrow( - 'Invalid --api-source "arcade"' + it("rejects removed legacy sources", () => { + expect(() => resolveApiSource({ apiSource: "tool-metadata" })).toThrow( + 'Invalid --api-source "tool-metadata"' + ); + expect(() => resolveApiSource({ apiSource: "list-tools" })).toThrow( + 'Invalid --api-source "list-tools"' ); }); - it("auto-selects public-catalog when only the Engine URL is set", () => { - process.env.ENGINE_API_URL = "https://api.arcade.dev"; + it("auto-selects public-catalog when an API URL is set", () => { + process.env.ARCADE_API_URL = "https://api.arcade.dev"; expect(resolveApiSource({})).toBe("public-catalog"); }); - it("auto-selects tool-metadata when Engine credentials exist", () => { - process.env.ENGINE_API_KEY = "test-key"; + it("falls back to ENGINE_API_URL for the API host", () => { process.env.ENGINE_API_URL = "https://api.arcade.dev"; - expect(resolveApiSource({})).toBe("tool-metadata"); + expect(resolveApiBaseUrlFromEnv()).toBe("https://api.arcade.dev"); + expect(resolveApiSource({})).toBe("public-catalog"); }); - it("does not auto-select list-tools from Arcade credentials", () => { - process.env.ARCADE_API_KEY = "test-key"; - process.env.ARCADE_API_URL = "https://api.arcade.dev"; - + it("defaults to mock when no API URL is configured", () => { expect(resolveApiSource({})).toBe("mock"); }); }); diff --git a/toolkit-docs-generator/tests/merger/data-merger.test.ts b/toolkit-docs-generator/tests/merger/data-merger.test.ts index d8e0a2940..0d55d1664 100644 --- a/toolkit-docs-generator/tests/merger/data-merger.test.ts +++ b/toolkit-docs-generator/tests/merger/data-merger.test.ts @@ -5,7 +5,7 @@ * the merge logic works correctly. */ import { describe, expect, it, vi } from "vitest"; -import type { ISecretEditGenerator } from "../../src/llm/secret-edit-generator"; +import type { SecretEditGenerator } from "../../src/llm/secret-edit-generator"; import { computeAllScopes, DataMerger, @@ -26,8 +26,8 @@ import { import type { ICustomSectionsSource } from "../../src/sources/interfaces"; import { createCombinedToolkitDataSource, - type IToolkitDataSource, type ToolkitData, + type ToolkitDataSource, } from "../../src/sources/toolkit-data-source"; import type { CustomSections, @@ -1532,7 +1532,7 @@ describe("DataMerger", () => { }, ]; - const cleanupSpy = vi.fn( + const cleanupSpy = vi.fn( async () => "| Secret | Required For |\n| `GITHUB_SERVER_URL` | All tools |" ); @@ -1540,7 +1540,7 @@ describe("DataMerger", () => { async (input: { content: string }) => `${input.content}\n\n[config link]` ); - const secretEditGenerator: ISecretEditGenerator = { + const secretEditGenerator: SecretEditGenerator = { cleanupStaleReferences: cleanupSpy, fillCoverageGaps: coverageSpy, }; @@ -1674,7 +1674,7 @@ describe("DataMerger", () => { const coverageSpy = vi.fn( async (input: { content: string }) => `${input.content} [link]` ); - const secretEditGenerator: ISecretEditGenerator = { + const secretEditGenerator: SecretEditGenerator = { cleanupStaleReferences: cleanupSpy, fillCoverageGaps: coverageSpy, }; @@ -2110,7 +2110,7 @@ describe("DataMerger", () => { metadata: slackMetadata, }; - const toolkitDataSource: IToolkitDataSource = { + const toolkitDataSource: ToolkitDataSource = { fetchToolkitData: async (toolkitId: string) => { if (toolkitId === "Github") { return completeToolkitData; @@ -2390,7 +2390,7 @@ describe("DataMerger", () => { metadata: null, }; - const toolkitDataSource: IToolkitDataSource = { + const toolkitDataSource: ToolkitDataSource = { fetchToolkitData: async () => { throw new Error("not used by mergeAllToolkits"); }, @@ -2430,7 +2430,7 @@ describe("DataMerger", () => { metadata: null, }; - const toolkitDataSource: IToolkitDataSource = { + const toolkitDataSource: ToolkitDataSource = { fetchToolkitData: async () => missingMetadataToolkitData, fetchAllToolkitsData: async () => new Map([["Unknown", missingMetadataToolkitData]]), @@ -2469,7 +2469,7 @@ describe("DataMerger", () => { metadata: null, }; - const toolkitDataSource: IToolkitDataSource = { + const toolkitDataSource: ToolkitDataSource = { fetchToolkitData: async () => missingMetadataToolkitData, fetchAllToolkitsData: async () => new Map([["Unknown", missingMetadataToolkitData]]), diff --git a/toolkit-docs-generator/tests/sources/arcade-api.test.ts b/toolkit-docs-generator/tests/sources/arcade-api.test.ts deleted file mode 100644 index b09abb17e..000000000 --- a/toolkit-docs-generator/tests/sources/arcade-api.test.ts +++ /dev/null @@ -1,797 +0,0 @@ -import type { Mock } from "vitest"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - ArcadeApiSource, - createArcadeApiSource, - createProductionArcadeApiSource, -} from "../../src/sources/arcade-api"; -import type { ArcadeToolsResponse } from "../../src/sources/arcade-api-types"; - -// ============================================================================ -// Test Fixtures -// ============================================================================ - -/** - * Realistic fixture based on actual Arcade API response - */ -const createMockArcadeResponse = ( - items: ArcadeToolsResponse["items"], - totalCount?: number -): ArcadeToolsResponse => ({ - items, - limit: 100, - offset: 0, - page_count: 1, - total_count: totalCount ?? items.length, -}); - -const mockAirtableTool: ArcadeToolsResponse["items"][0] = { - fully_qualified_name: "AirtableApi.AddBaseCollaborator@4.0.0", - qualified_name: "AirtableApi.AddBaseCollaborator", - name: "AddBaseCollaborator", - description: - "Add a collaborator to an Airtable base.\n\nUse this tool to add a new collaborator to a specified Airtable base.", - toolkit: { - name: "AirtableApi", - description: - "Tools that enable LLMs to interact directly with the Airtable API.", - version: "4.0.0", - }, - input: { - parameters: [ - { - name: "mode", - required: true, - description: - "Operation mode: 'get_request_schema' returns the OpenAPI spec for the request body, 'execute' performs the actual operation", - value_schema: { - val_type: "string", - enum: ["get_request_schema", "execute"], - }, - inferrable: true, - }, - { - name: "base_id", - required: false, - description: - "The ID of the Airtable base to which the collaborator will be added.", - value_schema: { - val_type: "string", - }, - inferrable: true, - }, - { - name: "request_body", - required: false, - description: "Stringified JSON representing the request body.", - value_schema: { - val_type: "string", - }, - inferrable: true, - }, - ], - }, - output: { - available_modes: ["value", "error"], - description: "Response from the API endpoint 'add-base-collaborator'.", - value_schema: { - val_type: "json", - }, - }, - requirements: { - met: true, - authorization: { - id: "arcade-airtable", - provider_id: "airtable", - provider_type: "oauth2", - oauth2: { - scopes: ["workspacesAndBases:write"], - }, - status: "active", - }, - }, -}; - -const mockGoogleCalendarTool: ArcadeToolsResponse["items"][0] = { - fully_qualified_name: "GoogleCalendar.CreateEvent@1.0.0", - qualified_name: "GoogleCalendar.CreateEvent", - name: "CreateEvent", - description: "Create a new calendar event.", - toolkit: { - name: "GoogleCalendar", - description: "Tools for interacting with Google Calendar.", - version: "1.0.0", - }, - input: { - parameters: [ - { - name: "title", - required: true, - description: "The title of the event.", - value_schema: { - val_type: "string", - }, - inferrable: true, - }, - { - name: "start_time", - required: true, - description: "The start time of the event.", - value_schema: { - val_type: "string", - }, - inferrable: true, - }, - { - name: "attendees", - required: false, - description: "List of attendee emails.", - value_schema: { - val_type: "array", - inner_val_type: "string", - }, - inferrable: true, - }, - ], - }, - output: { - available_modes: ["value", "error"], - description: "The created event.", - value_schema: { - val_type: "json", - }, - }, - requirements: { - met: true, - authorization: { - id: "arcade-google", - provider_id: "google", - provider_type: "oauth2", - oauth2: { - scopes: [ - "https://www.googleapis.com/auth/calendar", - "https://www.googleapis.com/auth/calendar.events", - ], - }, - status: "active", - }, - secrets: [ - { - key: "GOOGLE_CLIENT_ID", - met: true, - }, - ], - }, -}; - -const mockToolWithSecrets: ArcadeToolsResponse["items"][0] = { - fully_qualified_name: "Stripe.CreatePayment@2.0.0", - qualified_name: "Stripe.CreatePayment", - name: "CreatePayment", - description: "Create a new payment.", - toolkit: { - name: "Stripe", - description: "Payment processing tools.", - version: "2.0.0", - }, - input: { - parameters: [ - { - name: "amount", - required: true, - description: "Amount in cents.", - value_schema: { - val_type: "integer", - }, - inferrable: true, - }, - ], - }, - output: { - available_modes: ["value", "error"], - description: "Payment result.", - value_schema: { - val_type: "json", - }, - }, - requirements: { - met: false, - secrets: [ - { - key: "STRIPE_API_KEY", - met: false, - status_reason: "Secret not configured", - }, - { - key: "STRIPE_WEBHOOK_SECRET", - met: false, - status_reason: "Secret not configured", - }, - ], - }, -}; - -// ============================================================================ -// Tests -// ============================================================================ - -/** - * ArcadeApiSource only reads `.ok`, `.status`, `.statusText`, `.headers.get(...)`, - * and `.json()` off the fetch response, so tests mock that subset and assert - * it as a `Response` rather than constructing a real one. - */ -type FetchResponseLike = { - ok: boolean; - status?: number; - statusText?: string; - headers?: { get(name: string): string | null | undefined }; - json?: () => Promise; -}; - -const asResponse = (value: FetchResponseLike): Response => - value as unknown as Response; - -describe("ArcadeApiSource", () => { - let mockFetch: Mock; - - beforeEach(() => { - mockFetch = vi.fn(); - }); - - describe("constructor and configuration", () => { - it("should create source with default page size", () => { - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - expect(source).toBeInstanceOf(ArcadeApiSource); - }); - - it("should normalize base URL with trailing slash", () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev/", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - source.fetchAllTools(); - - expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining("https://api.arcade.dev/v1/tools"), - expect.any(Object) - ); - }); - - it("should handle base URL with /v1 suffix", () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev/v1", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - source.fetchAllTools(); - - expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining("https://api.arcade.dev/v1/tools"), - expect.any(Object) - ); - }); - - it("should cap page size at maximum", () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - pageSize: 1500, // Over max of 1000 - fetchFn: mockFetch, - }); - - source.fetchAllTools(); - - const calledUrl = mockFetch.mock.calls[0]?.[0] as string; - expect(calledUrl).toContain("limit=1000"); // Capped at 1000 - }); - }); - - describe("fetchAllTools", () => { - it("should fetch and transform tools correctly", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve( - createMockArcadeResponse([ - mockAirtableTool, - mockGoogleCalendarTool, - ]) - ), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const tools = await source.fetchAllTools(); - - expect(tools).toHaveLength(2); - - // Check first tool transformation - const airtableTool = tools[0]; - expect(airtableTool).toEqual({ - name: "AddBaseCollaborator", - qualifiedName: "AirtableApi.AddBaseCollaborator", - fullyQualifiedName: "AirtableApi.AddBaseCollaborator@4.0.0", - description: expect.stringContaining("Add a collaborator"), - toolkitDescription: expect.stringContaining("Airtable API"), - parameters: [ - { - name: "mode", - type: "string", - innerType: undefined, - required: true, - description: expect.stringContaining("Operation mode"), - enum: ["get_request_schema", "execute"], - inferrable: true, - }, - { - name: "base_id", - type: "string", - innerType: undefined, - required: false, - description: expect.stringContaining("ID of the Airtable base"), - enum: null, - inferrable: true, - }, - { - name: "request_body", - type: "string", - innerType: undefined, - required: false, - description: expect.stringContaining("Stringified JSON"), - enum: null, - inferrable: true, - }, - ], - auth: { - providerId: "airtable", - providerType: "oauth2", - scopes: ["workspacesAndBases:write"], - }, - secrets: [], - output: { - type: "json", - description: expect.stringContaining("add-base-collaborator"), - }, - }); - }); - - it("should handle tools with array parameters", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const tools = await source.fetchAllTools(); - const attendeesParam = tools[0]?.parameters.find( - (p) => p.name === "attendees" - ); - - expect(attendeesParam).toEqual({ - name: "attendees", - type: "array", - innerType: "string", - required: false, - description: "List of attendee emails.", - enum: null, - inferrable: true, - }); - }); - - it("should extract secrets from requirements", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve(createMockArcadeResponse([mockToolWithSecrets])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const tools = await source.fetchAllTools(); - - expect(tools[0]?.secrets).toEqual([ - "STRIPE_API_KEY", - "STRIPE_WEBHOOK_SECRET", - ]); - }); - - it("should filter by toolkit ID client-side", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve( - createMockArcadeResponse([ - mockAirtableTool, - mockGoogleCalendarTool, - mockToolWithSecrets, - ]) - ), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const tools = await source.fetchAllTools({ toolkitId: "GoogleCalendar" }); - - expect(tools).toHaveLength(1); - expect(tools[0]?.qualifiedName).toBe("GoogleCalendar.CreateEvent"); - }); - - it("should filter by toolkit ID case-insensitively", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const tools = await source.fetchAllTools({ toolkitId: "googlecalendar" }); - - expect(tools).toHaveLength(1); - }); - - it("should filter by version", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve( - createMockArcadeResponse([ - mockAirtableTool, // @4.0.0 - mockGoogleCalendarTool, // @1.0.0 - mockToolWithSecrets, // @2.0.0 - ]) - ), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const tools = await source.fetchAllTools({ version: "4.0.0" }); - - expect(tools).toHaveLength(1); - expect(tools[0]?.fullyQualifiedName).toBe( - "AirtableApi.AddBaseCollaborator@4.0.0" - ); - }); - }); - - describe("pagination", () => { - it("should handle pagination correctly", async () => { - // First page - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve({ - items: [mockAirtableTool], - limit: 1, - offset: 0, - total_count: 2, - }), - }) - ); - - // Second page - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve({ - items: [mockGoogleCalendarTool], - limit: 1, - offset: 1, - total_count: 2, - }), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - pageSize: 1, // Force pagination - fetchFn: mockFetch, - }); - - const tools = await source.fetchAllTools(); - - expect(mockFetch).toHaveBeenCalledTimes(2); - expect(tools).toHaveLength(2); - }); - - it("should stop pagination when no more items", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve({ - items: [mockAirtableTool], - limit: 100, - offset: 0, - total_count: 100, // Says 100 but only returns 1 - }), - }) - ); - - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve({ - items: [], - limit: 100, - offset: 1, - total_count: 100, - }), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const tools = await source.fetchAllTools(); - - expect(tools).toHaveLength(1); - }); - }); - - describe("fetchToolsByToolkit", () => { - it("should call fetchAllTools with toolkit filter", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => - Promise.resolve(createMockArcadeResponse([mockGoogleCalendarTool])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const tools = await source.fetchToolsByToolkit("GoogleCalendar"); - - expect(tools).toHaveLength(1); - expect(tools[0]?.qualifiedName).toBe("GoogleCalendar.CreateEvent"); - }); - }); - - describe("isAvailable", () => { - it("should return true when API is accessible", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const available = await source.isAvailable(); - expect(available).toBe(true); - }); - - it("should return false when API returns error", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: false, - status: 401, - statusText: "Unauthorized", - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const available = await source.isAvailable(); - expect(available).toBe(false); - }); - - it("should return false when fetch throws", async () => { - mockFetch.mockRejectedValueOnce(new Error("Network error")); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - const available = await source.isAvailable(); - expect(available).toBe(false); - }); - }); - - describe("error handling", () => { - it("should throw on API error with JSON detail", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: false, - status: 401, - statusText: "Unauthorized", - headers: new Map([["content-type", "application/json"]]), - json: () => Promise.resolve({ detail: "Invalid API key" }), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "invalid-key", - fetchFn: mockFetch, - }); - - await expect(source.fetchAllTools()).rejects.toThrow( - "Arcade API error 401: Invalid API key" - ); - }); - - it("should throw on API error without JSON detail", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: false, - status: 500, - statusText: "Internal Server Error", - headers: new Map([["content-type", "text/plain"]]), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - await expect(source.fetchAllTools()).rejects.toThrow( - "Arcade API error 500: Internal Server Error" - ); - }); - - it("should throw on invalid response schema", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => Promise.resolve({ invalid: "response" }), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - fetchFn: mockFetch, - }); - - await expect(source.fetchAllTools()).rejects.toThrow( - "Invalid Arcade API response" - ); - }); - }); - - describe("authorization header", () => { - it("should include Bearer token in request", async () => { - mockFetch.mockResolvedValueOnce( - asResponse({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }) - ); - - const source = new ArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "my-secret-key", - fetchFn: mockFetch, - }); - - await source.fetchAllTools(); - - expect(mockFetch).toHaveBeenCalledWith(expect.any(String), { - headers: { - Authorization: "Bearer my-secret-key", - Accept: "application/json", - }, - }); - }); - }); -}); - -describe("Factory functions", () => { - it("createArcadeApiSource should create source instance", () => { - const source = createArcadeApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test-key", - }); - expect(source).toBeInstanceOf(ArcadeApiSource); - }); - - it("createProductionArcadeApiSource should use default URL", () => { - const mockFetch = vi.fn().mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(createMockArcadeResponse([])), - }); - - const source = createProductionArcadeApiSource("test-key", { - fetchFn: mockFetch, - }); - - source.fetchAllTools(); - - expect(mockFetch).toHaveBeenCalledWith( - expect.stringContaining("https://api.arcade.dev/v1/tools"), - expect.any(Object) - ); - }); -}); diff --git a/toolkit-docs-generator/tests/sources/engine-api.test.ts b/toolkit-docs-generator/tests/sources/engine-api.test.ts deleted file mode 100644 index 6056fbfcc..000000000 --- a/toolkit-docs-generator/tests/sources/engine-api.test.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { EngineApiSource } from "../../src/sources/engine-api"; - -type ToolMetadataItem = { - fully_qualified_name: string; - qualified_name: string; - name: string; - description: string | null; - toolkit: { - name: string; - version: string; - description: string | null; - }; - input: { - parameters: Array<{ - name: string; - required: boolean; - description: string | null; - value_schema: { - val_type: string; - inner_val_type: string | null; - enum: string[] | null; - }; - inferrable: boolean; - }>; - }; - output?: { - description: string | null; - value_schema: { - val_type: string; - inner_val_type: string | null; - enum: string[] | null; - } | null; - } | null; - requirements?: { - authorization: Array<{ - id?: string | null; - provider_id: string | null; - provider_type: string | null; - scopes: string[]; - }> | null; - secrets: Array<{ key: string }>; - } | null; - metadata?: { - classification?: { - service_domains?: string[]; - } | null; - behavior?: { - operations?: string[]; - read_only?: boolean; - destructive?: boolean; - idempotent?: boolean; - open_world?: boolean; - } | null; - extras?: Record | null; - } | null; -}; - -const createItems = (): ToolMetadataItem[] => [ - { - fully_qualified_name: "Github.CreateIssue@1.0.0", - qualified_name: "Github.CreateIssue", - name: "CreateIssue", - description: "Create issue", - toolkit: { - name: "Github", - version: "1.0.0", - description: "GitHub toolkit", - }, - input: { parameters: [] }, - output: null, - requirements: { - authorization: [ - { - provider_id: "github", - provider_type: "oauth2", - scopes: ["repo"], - }, - ], - secrets: [{ key: "GITHUB_API_KEY" }], - }, - }, - { - fully_qualified_name: "Slack.SendMessage@1.2.0", - qualified_name: "Slack.SendMessage", - name: "SendMessage", - description: "Send message", - toolkit: { - name: "Slack", - version: "1.2.0", - description: "Slack toolkit", - }, - input: { parameters: [] }, - output: { - description: "Confirmation", - value_schema: null, - }, - requirements: { - authorization: [ - { - provider_id: null, - provider_type: "oauth2", - scopes: ["chat:write"], - }, - ], - secrets: [], - }, - }, -]; - -const createFetchStub = - (items: ToolMetadataItem[], status = 200) => - async (input: string | URL | Request) => { - if (status !== 200) { - return new Response("error", { status }); - } - - const url = new URL(input.toString()); - const limit = Number(url.searchParams.get("limit") ?? items.length); - const offset = Number(url.searchParams.get("offset") ?? 0); - const toolkit = url.searchParams.get("toolkit"); - const authProvider = url.searchParams.get("auth_provider"); - const version = url.searchParams.get("version"); - - let filtered = items; - if (toolkit) { - filtered = filtered.filter((item) => item.toolkit.name === toolkit); - } - if (authProvider) { - filtered = filtered.filter((item) => - item.requirements?.authorization?.some( - (auth) => auth.provider_id === authProvider - ) - ); - } - if (version) { - filtered = filtered.filter((item) => - item.fully_qualified_name.endsWith(`@${version}`) - ); - } - - const pageItems = filtered.slice(offset, offset + limit); - - return new Response( - JSON.stringify({ - items: pageItems, - total_count: filtered.length, - }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - } - ); - }; - -const createErrorFetchStub = (status: number, payload: unknown) => async () => - new Response(JSON.stringify(payload), { - status, - headers: { "Content-Type": "application/json" }, - }); - -const createInspectFetchStub = - (inspect: (params: URLSearchParams) => void) => - async (input: string | URL | Request) => { - const url = new URL(input.toString()); - inspect(url.searchParams); - return new Response( - JSON.stringify({ - items: [], - total_count: 0, - }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - } - ); - }; - -const createSummaryFetchStub = - (payload: unknown, inspect?: (url: URL) => void) => - async (input: string | URL | Request) => { - const url = new URL(input.toString()); - inspect?.(url); - return new Response(JSON.stringify(payload), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }; - -const createInvalidJsonFetchStub = - (contentType = "application/json") => - async () => - new Response("not-json", { - status: 200, - headers: { "Content-Type": contentType }, - }); - -describe("EngineApiSource", () => { - it("fetches and transforms tool metadata with pagination", async () => { - const items = createItems(); - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - pageSize: 1, - fetchFn: createFetchStub(items), - }); - - const tools = await source.fetchAllTools(); - - expect(tools).toHaveLength(2); - expect(tools[0]?.toolkitDescription).toBe("GitHub toolkit"); - expect(tools[0]?.secrets).toEqual(["GITHUB_API_KEY"]); - expect(tools[1]?.output?.type).toBe("string"); - expect(tools[1]?.auth?.providerId).toBeNull(); - }); - - it("handles tool metadata output objects with missing fields", async () => { - const items: ToolMetadataItem[] = [ - { - fully_qualified_name: "Github.CreateIssue@1.0.0", - qualified_name: "Github.CreateIssue", - name: "CreateIssue", - description: "Create issue", - toolkit: { - name: "Github", - version: "1.0.0", - description: "GitHub toolkit", - }, - input: { parameters: [] }, - output: {} as NonNullable, - requirements: { - authorization: null, - secrets: [], - }, - }, - ]; - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createFetchStub(items), - }); - - const tools = await source.fetchAllTools(); - - expect(tools).toHaveLength(1); - expect(tools[0]?.output).toEqual({ - type: "string", - description: null, - }); - }); - - it("normalizes empty enum arrays to null", async () => { - const items: ToolMetadataItem[] = [ - { - fully_qualified_name: "Github.CreateIssue@1.0.0", - qualified_name: "Github.CreateIssue", - name: "CreateIssue", - description: "Create issue", - toolkit: { - name: "Github", - version: "1.0.0", - description: "GitHub toolkit", - }, - input: { - parameters: [ - { - name: "mode", - required: true, - description: "Execution mode", - value_schema: { - val_type: "string", - inner_val_type: null, - enum: [], - }, - inferrable: true, - }, - ], - }, - output: null, - requirements: { - authorization: null, - secrets: [], - }, - }, - ]; - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createFetchStub(items), - }); - - const tools = await source.fetchAllTools(); - - expect(tools[0]?.parameters[0]?.enum).toBeNull(); - }); - - it("filters tools by toolkit and provider", async () => { - const items = createItems(); - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createFetchStub(items), - }); - - const tools = await source.fetchAllTools({ - toolkitId: "Github", - providerId: "github", - }); - - expect(tools).toHaveLength(1); - expect(tools[0]?.qualifiedName).toBe("Github.CreateIssue"); - }); - - it("returns false when the endpoint is not available", async () => { - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createFetchStub(createItems(), 500), - }); - - const available = await source.isAvailable(); - - expect(available).toBe(false); - }); - - it("includes error details from API responses", async () => { - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createErrorFetchStub(400, { - name: "BadRequest", - message: "Invalid query", - }), - }); - - await expect(source.fetchAllTools()).rejects.toThrow( - "BadRequest: Invalid query" - ); - }); - - it("clamps page size to the maximum limit", async () => { - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - pageSize: 1500, // Over max of 1000 - fetchFn: createInspectFetchStub((params) => { - expect(params.get("limit")).toBe("1000"); // Capped at 1000 - }), - }); - - await source.fetchAllTools(); - }); - - it("sets latest_only=false when filtering by version", async () => { - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createInspectFetchStub((params) => { - expect(params.get("latest_only")).toBe("false"); - expect(params.get("version")).toBe("0.1.3"); - }), - }); - - await source.fetchAllTools({ version: "0.1.3" }); - }); - - it("fetches toolkit summary from the summary endpoint", async () => { - const summaryPayload = { - total_tools: 2, - total_toolkits: 1, - starter_toolkits: 0, - toolkits: [ - { - name: "Github", - version: "1.0.0", - tool_count: 2, - requires_secrets: true, - requires_oauth: true, - }, - ], - }; - - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createSummaryFetchStub(summaryPayload, (url) => { - expect(url.pathname).toBe("/v1/tool_metadata_summary"); - expect(url.searchParams.get("mode")).toBeNull(); - }), - }); - - const summary = await source.fetchToolkitsSummary(); - - expect(summary.totalToolkits).toBe(1); - expect(summary.toolkits[0]).toEqual({ - name: "Github", - version: "1.0.0", - toolCount: 2, - requiresSecrets: true, - requiresOauth: true, - }); - }); - - it("throws a clear error when tool metadata response is invalid JSON", async () => { - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createInvalidJsonFetchStub(), - }); - - await expect(source.fetchAllTools()).rejects.toThrow( - "Engine API returned invalid JSON for tool metadata" - ); - }); - - it("throws a clear error when summary response is invalid JSON", async () => { - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createInvalidJsonFetchStub(), - }); - - await expect(source.fetchToolkitsSummary()).rejects.toThrow( - "Engine API returned invalid JSON for tool metadata summary" - ); - }); - - it("parses per-tool metadata fields and maps snake_case to camelCase", async () => { - const items: ToolMetadataItem[] = [ - { - fully_qualified_name: "Github.CreateIssue@1.0.0", - qualified_name: "Github.CreateIssue", - name: "CreateIssue", - description: "Create issue", - toolkit: { - name: "Github", - version: "1.0.0", - description: "GitHub toolkit", - }, - input: { parameters: [] }, - output: null, - requirements: { authorization: [], secrets: [] }, - metadata: { - classification: { service_domains: ["github", "git"] }, - behavior: { - operations: ["read", "write"], - read_only: false, - destructive: false, - idempotent: true, - open_world: false, - }, - extras: { custom_key: "value" }, - }, - }, - ]; - - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createFetchStub(items), - }); - - const tools = await source.fetchAllTools(); - - expect(tools).toHaveLength(1); - expect(tools[0]?.metadata).toEqual({ - classification: { serviceDomains: ["github", "git"] }, - behavior: { - operations: ["read", "write"], - readOnly: false, - destructive: false, - idempotent: true, - openWorld: false, - }, - extras: { custom_key: "value" }, - }); - }); - - it("sets metadata to null when the API does not provide it", async () => { - const items = createItems(); - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createFetchStub(items), - }); - - const tools = await source.fetchAllTools(); - - expect(tools[0]?.metadata).toBeNull(); - }); - - it("handles partial metadata (behavior present, classification absent)", async () => { - const items: ToolMetadataItem[] = [ - { - fully_qualified_name: "Github.CreateIssue@1.0.0", - qualified_name: "Github.CreateIssue", - name: "CreateIssue", - description: "Create issue", - toolkit: { - name: "Github", - version: "1.0.0", - description: "GitHub toolkit", - }, - input: { parameters: [] }, - output: null, - requirements: { authorization: [], secrets: [] }, - metadata: { - classification: null, - behavior: { operations: ["read"] }, - extras: null, - }, - }, - ]; - - const source = new EngineApiSource({ - baseUrl: "https://api.arcade.dev", - apiKey: "test", - fetchFn: createFetchStub(items), - }); - - const tools = await source.fetchAllTools(); - - expect(tools[0]?.metadata?.classification.serviceDomains).toEqual([]); - expect(tools[0]?.metadata?.behavior.operations).toEqual(["read"]); - expect(tools[0]?.metadata?.extras).toBeNull(); - }); -}); diff --git a/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts b/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts index 4d3f3fdd5..bfe1d4cef 100644 --- a/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts +++ b/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts @@ -10,11 +10,11 @@ import { InMemoryMetadataSource, InMemoryToolDataSource, } from "../../src/sources/in-memory"; -import type { IMetadataSource } from "../../src/sources/internal"; +import type { MetadataSource } from "../../src/sources/internal"; import { createCachedToolkitDataSource, createCombinedToolkitDataSource, - type IToolkitDataSource, + type ToolkitDataSource, } from "../../src/sources/toolkit-data-source"; import type { ToolDefinition, ToolkitMetadata } from "../../src/types/index"; @@ -334,7 +334,7 @@ describe("CombinedToolkitDataSource", () => { async listToolkitIds() { return []; }, - } satisfies IMetadataSource; + } satisfies MetadataSource; const dataSource = createCombinedToolkitDataSource({ toolSource, @@ -367,7 +367,7 @@ describe("CombinedToolkitDataSource", () => { async listToolkitIds() { return ["Slack"]; }, - } satisfies IMetadataSource; + } satisfies MetadataSource; const dataSource = createCombinedToolkitDataSource({ toolSource, @@ -398,7 +398,7 @@ describe("CombinedToolkitDataSource", () => { async listToolkitIds() { return ["Github"]; }, - } satisfies IMetadataSource; + } satisfies MetadataSource; const dataSource = createCombinedToolkitDataSource({ toolSource, @@ -427,7 +427,7 @@ describe("createCachedToolkitDataSource", () => { throw new Error("Expected GitHub fixture"); } let fetchAllCalls = 0; - const source: IToolkitDataSource = { + const source: ToolkitDataSource = { fetchToolkitData: async () => githubData, fetchAllToolkitsData: async () => { fetchAllCalls += 1; diff --git a/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts b/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts index e31845ef3..b4fba1f20 100644 --- a/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts +++ b/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts @@ -30,8 +30,9 @@ test("porter workflow generates docs and opens a PR", () => { expect(workflowContents).toContain("--preserve-last-known-good"); expect(workflowContents).toContain("--verbose"); expect(workflowContents).toContain("--api-source public-catalog"); - expect(workflowContents).toContain("--tool-metadata-url"); + expect(workflowContents).toContain("--api-url"); expect(workflowContents).not.toContain("--tool-metadata-key"); + expect(workflowContents).not.toContain("--tool-metadata-url"); expect(workflowContents).toContain("--llm-provider anthropic"); expect(workflowContents).toContain("--llm-model"); expect(workflowContents).toContain("--llm-api-key");