From ff60a080d09baf94225e0d6497107cc55ba7e3bc Mon Sep 17 00:00:00 2001 From: e-roy Date: Fri, 5 Sep 2025 14:14:29 -0400 Subject: [PATCH 01/21] testing openai reduced wrapper --- packages/tools/package.json | 2 +- .../tools/src/utils/openai-tool-wrapper.ts | 180 ++++++++++-------- 2 files changed, 100 insertions(+), 82 deletions(-) diff --git a/packages/tools/package.json b/packages/tools/package.json index 7b61148..24054fe 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.10", + "version": "0.0.11.beta.1", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/utils/openai-tool-wrapper.ts b/packages/tools/src/utils/openai-tool-wrapper.ts index 3ca1eff..f82c556 100644 --- a/packages/tools/src/utils/openai-tool-wrapper.ts +++ b/packages/tools/src/utils/openai-tool-wrapper.ts @@ -12,87 +12,6 @@ export interface OpenAIToolConfig> { export function createOpenAITool>(config: OpenAIToolConfig) { const { name, description, inputSchema, execute } = config; - const properties: Record = {}; - const required: string[] = []; - - // Extract properties from Zod schema using type guards - if (inputSchema instanceof z.ZodObject) { - const shape = inputSchema.shape; - Object.entries(shape).forEach(([key, schema]) => { - let isRequired = true; - let actualSchema: z.ZodType = schema as z.ZodType; - let fieldDescription = ''; - - // Extract description from the original schema - if (schema && typeof schema === 'object' && 'description' in schema) { - const desc = (schema as any).description; - if (typeof desc === 'string') { - fieldDescription = desc; - } - } - - // Handle optional fields - if (schema instanceof z.ZodOptional) { - isRequired = false; - actualSchema = schema.unwrap(); - // If no description was found on the optional wrapper, try the unwrapped schema - if (!fieldDescription && actualSchema.description) { - fieldDescription = actualSchema.description; - } - } - - // Handle default values (they make fields optional) - if (schema instanceof z.ZodDefault) { - isRequired = false; - actualSchema = schema.removeDefault(); - // If no description was found on the default wrapper, try the unwrapped schema - if (!fieldDescription && actualSchema.description) { - fieldDescription = actualSchema.description; - } - } - - // Map Zod types to OpenAI parameter types - if (actualSchema instanceof z.ZodString) { - properties[key] = { - type: 'string', - description: fieldDescription || `${key} parameter`, - }; - } else if (actualSchema instanceof z.ZodEnum) { - properties[key] = { - type: 'string', - enum: actualSchema._def.values, - description: fieldDescription || `${key} parameter`, - }; - } else if (actualSchema instanceof z.ZodNumber) { - properties[key] = { - type: 'number', - description: fieldDescription || `${key} parameter`, - }; - } else if (actualSchema instanceof z.ZodBoolean) { - properties[key] = { - type: 'boolean', - description: fieldDescription || `${key} parameter`, - }; - } else if (actualSchema instanceof z.ZodArray) { - properties[key] = { - type: 'array', - items: { type: 'string' }, // Default to string array, can be enhanced - description: fieldDescription || `${key} parameter`, - }; - } else { - // Fallback for unknown types - properties[key] = { - type: 'string', - description: fieldDescription || `${key} parameter`, - }; - } - - if (isRequired) { - required.push(key); - } - }); - } - return tool({ name, description, @@ -110,4 +29,103 @@ export function createOpenAITool>(config: OpenAIToolC } }, }); + + // const properties: Record = {}; + // const required: string[] = []; + + // // Extract properties from Zod schema using type guards + // if (inputSchema instanceof z.ZodObject) { + // const shape = inputSchema.shape; + // Object.entries(shape).forEach(([key, schema]) => { + // let isRequired = true; + // let actualSchema: z.ZodType = schema as z.ZodType; + // let fieldDescription = ''; + + // // Extract description from the original schema + // if (schema && typeof schema === 'object' && 'description' in schema) { + // const desc = (schema as any).description; + // if (typeof desc === 'string') { + // fieldDescription = desc; + // } + // } + + // // Handle optional fields + // if (schema instanceof z.ZodOptional) { + // isRequired = false; + // actualSchema = schema.unwrap(); + // // If no description was found on the optional wrapper, try the unwrapped schema + // if (!fieldDescription && actualSchema.description) { + // fieldDescription = actualSchema.description; + // } + // } + + // // Handle default values (they make fields optional) + // if (schema instanceof z.ZodDefault) { + // isRequired = false; + // actualSchema = schema.removeDefault(); + // // If no description was found on the default wrapper, try the unwrapped schema + // if (!fieldDescription && actualSchema.description) { + // fieldDescription = actualSchema.description; + // } + // } + + // // Map Zod types to OpenAI parameter types + // if (actualSchema instanceof z.ZodString) { + // properties[key] = { + // type: 'string', + // description: fieldDescription || `${key} parameter`, + // }; + // } else if (actualSchema instanceof z.ZodEnum) { + // properties[key] = { + // type: 'string', + // enum: actualSchema._def.values, + // description: fieldDescription || `${key} parameter`, + // }; + // } else if (actualSchema instanceof z.ZodNumber) { + // properties[key] = { + // type: 'number', + // description: fieldDescription || `${key} parameter`, + // }; + // } else if (actualSchema instanceof z.ZodBoolean) { + // properties[key] = { + // type: 'boolean', + // description: fieldDescription || `${key} parameter`, + // }; + // } else if (actualSchema instanceof z.ZodArray) { + // properties[key] = { + // type: 'array', + // items: { type: 'string' }, // Default to string array, can be enhanced + // description: fieldDescription || `${key} parameter`, + // }; + // } else { + // // Fallback for unknown types + // properties[key] = { + // type: 'string', + // description: fieldDescription || `${key} parameter`, + // }; + // } + + // if (isRequired) { + // required.push(key); + // } + // }); + // } + + // return tool({ + // name, + // description, + // parameters: inputSchema as any, + // strict: true, + // execute: async (input: unknown) => { + // try { + // const validatedInput = inputSchema.parse(input); + // return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); + // } catch (error) { + // if (error instanceof z.ZodError) { + // return `Invalid input: ${error.errors.map(e => e.message).join(', ')}`; + // } + // return `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`; + // } + // }, + // }); } From 8bba058409a1490a667ad6916b9504d95c171b2a Mon Sep 17 00:00:00 2001 From: e-roy Date: Fri, 5 Sep 2025 14:27:09 -0400 Subject: [PATCH 02/21] add different zod converter --- packages/tools/package.json | 2 +- .../tools/src/utils/openai-tool-wrapper.ts | 175 +++++++++--------- 2 files changed, 84 insertions(+), 93 deletions(-) diff --git a/packages/tools/package.json b/packages/tools/package.json index 24054fe..9c09499 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11.beta.1", + "version": "0.0.11-beta.2", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/utils/openai-tool-wrapper.ts b/packages/tools/src/utils/openai-tool-wrapper.ts index f82c556..b860fbb 100644 --- a/packages/tools/src/utils/openai-tool-wrapper.ts +++ b/packages/tools/src/utils/openai-tool-wrapper.ts @@ -12,10 +12,13 @@ export interface OpenAIToolConfig> { export function createOpenAITool>(config: OpenAIToolConfig) { const { name, description, inputSchema, execute } = config; + // Convert Zod schema to JSON Schema + const jsonSchema = zodToJsonSchema(inputSchema); + return tool({ name, description, - parameters: inputSchema as any, + parameters: jsonSchema, strict: true, execute: async (input: unknown) => { try { @@ -29,103 +32,91 @@ export function createOpenAITool>(config: OpenAIToolC } }, }); +} - // const properties: Record = {}; - // const required: string[] = []; +// Helper function to convert Zod schema to JSON Schema +function zodToJsonSchema(schema: z.ZodObject): any { + const shape = schema.shape; + const properties: Record = {}; + const required: string[] = []; - // // Extract properties from Zod schema using type guards - // if (inputSchema instanceof z.ZodObject) { - // const shape = inputSchema.shape; - // Object.entries(shape).forEach(([key, schema]) => { - // let isRequired = true; - // let actualSchema: z.ZodType = schema as z.ZodType; - // let fieldDescription = ''; + Object.entries(shape).forEach(([key, fieldSchema]) => { + let isRequired = true; + let actualSchema: z.ZodType = fieldSchema as z.ZodType; + let fieldDescription = ''; - // // Extract description from the original schema - // if (schema && typeof schema === 'object' && 'description' in schema) { - // const desc = (schema as any).description; - // if (typeof desc === 'string') { - // fieldDescription = desc; - // } - // } + // Extract description from the original schema + if (fieldSchema && typeof fieldSchema === 'object' && 'description' in fieldSchema) { + const desc = (fieldSchema as any).description; + if (typeof desc === 'string') { + fieldDescription = desc; + } + } - // // Handle optional fields - // if (schema instanceof z.ZodOptional) { - // isRequired = false; - // actualSchema = schema.unwrap(); - // // If no description was found on the optional wrapper, try the unwrapped schema - // if (!fieldDescription && actualSchema.description) { - // fieldDescription = actualSchema.description; - // } - // } + // Handle optional fields + if (fieldSchema instanceof z.ZodOptional) { + isRequired = false; + actualSchema = fieldSchema.unwrap(); + // If no description was found on the optional wrapper, try the unwrapped schema + if (!fieldDescription && actualSchema.description) { + fieldDescription = actualSchema.description; + } + } - // // Handle default values (they make fields optional) - // if (schema instanceof z.ZodDefault) { - // isRequired = false; - // actualSchema = schema.removeDefault(); - // // If no description was found on the default wrapper, try the unwrapped schema - // if (!fieldDescription && actualSchema.description) { - // fieldDescription = actualSchema.description; - // } - // } + // Handle default values (they make fields optional) + if (fieldSchema instanceof z.ZodDefault) { + isRequired = false; + actualSchema = fieldSchema.removeDefault(); + // If no description was found on the default wrapper, try the unwrapped schema + if (!fieldDescription && actualSchema.description) { + fieldDescription = actualSchema.description; + } + } - // // Map Zod types to OpenAI parameter types - // if (actualSchema instanceof z.ZodString) { - // properties[key] = { - // type: 'string', - // description: fieldDescription || `${key} parameter`, - // }; - // } else if (actualSchema instanceof z.ZodEnum) { - // properties[key] = { - // type: 'string', - // enum: actualSchema._def.values, - // description: fieldDescription || `${key} parameter`, - // }; - // } else if (actualSchema instanceof z.ZodNumber) { - // properties[key] = { - // type: 'number', - // description: fieldDescription || `${key} parameter`, - // }; - // } else if (actualSchema instanceof z.ZodBoolean) { - // properties[key] = { - // type: 'boolean', - // description: fieldDescription || `${key} parameter`, - // }; - // } else if (actualSchema instanceof z.ZodArray) { - // properties[key] = { - // type: 'array', - // items: { type: 'string' }, // Default to string array, can be enhanced - // description: fieldDescription || `${key} parameter`, - // }; - // } else { - // // Fallback for unknown types - // properties[key] = { - // type: 'string', - // description: fieldDescription || `${key} parameter`, - // }; - // } + // Map Zod types to JSON Schema types + if (actualSchema instanceof z.ZodString) { + properties[key] = { + type: 'string', + description: fieldDescription || `${key} parameter`, + }; + } else if (actualSchema instanceof z.ZodEnum) { + properties[key] = { + type: 'string', + enum: actualSchema._def.values, + description: fieldDescription || `${key} parameter`, + }; + } else if (actualSchema instanceof z.ZodNumber) { + properties[key] = { + type: 'number', + description: fieldDescription || `${key} parameter`, + }; + } else if (actualSchema instanceof z.ZodBoolean) { + properties[key] = { + type: 'boolean', + description: fieldDescription || `${key} parameter`, + }; + } else if (actualSchema instanceof z.ZodArray) { + properties[key] = { + type: 'array', + items: { type: 'string' }, // Default to string array, can be enhanced + description: fieldDescription || `${key} parameter`, + }; + } else { + // Fallback for unknown types + properties[key] = { + type: 'string', + description: fieldDescription || `${key} parameter`, + }; + } - // if (isRequired) { - // required.push(key); - // } - // }); - // } + if (isRequired) { + required.push(key); + } + }); - // return tool({ - // name, - // description, - // parameters: inputSchema as any, - // strict: true, - // execute: async (input: unknown) => { - // try { - // const validatedInput = inputSchema.parse(input); - // return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); - // } catch (error) { - // if (error instanceof z.ZodError) { - // return `Invalid input: ${error.errors.map(e => e.message).join(', ')}`; - // } - // return `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`; - // } - // }, - // }); + return { + type: 'object', + properties, + required, + }; } From 9ef80f32fba95e8b6d11b3e4104b5167a336361a Mon Sep 17 00:00:00 2001 From: e-roy Date: Fri, 5 Sep 2025 14:34:06 -0400 Subject: [PATCH 03/21] reduced code and add additionalProperties --- .../tools/src/utils/openai-tool-wrapper.ts | 104 +++--------------- 1 file changed, 14 insertions(+), 90 deletions(-) diff --git a/packages/tools/src/utils/openai-tool-wrapper.ts b/packages/tools/src/utils/openai-tool-wrapper.ts index b860fbb..659c53a 100644 --- a/packages/tools/src/utils/openai-tool-wrapper.ts +++ b/packages/tools/src/utils/openai-tool-wrapper.ts @@ -12,13 +12,24 @@ export interface OpenAIToolConfig> { export function createOpenAITool>(config: OpenAIToolConfig) { const { name, description, inputSchema, execute } = config; - // Convert Zod schema to JSON Schema - const jsonSchema = zodToJsonSchema(inputSchema); + // Create a simple JSON schema from the Zod schema + const properties: Record = {}; + const required: string[] = []; + + Object.entries(inputSchema.shape).forEach(([key, _fieldSchema]) => { + properties[key] = { type: 'string' }; + required.push(key); + }); return tool({ name, description, - parameters: jsonSchema, + parameters: { + type: 'object', + properties, + required, + additionalProperties: false, + }, strict: true, execute: async (input: unknown) => { try { @@ -33,90 +44,3 @@ export function createOpenAITool>(config: OpenAIToolC }, }); } - -// Helper function to convert Zod schema to JSON Schema -function zodToJsonSchema(schema: z.ZodObject): any { - const shape = schema.shape; - const properties: Record = {}; - const required: string[] = []; - - Object.entries(shape).forEach(([key, fieldSchema]) => { - let isRequired = true; - let actualSchema: z.ZodType = fieldSchema as z.ZodType; - let fieldDescription = ''; - - // Extract description from the original schema - if (fieldSchema && typeof fieldSchema === 'object' && 'description' in fieldSchema) { - const desc = (fieldSchema as any).description; - if (typeof desc === 'string') { - fieldDescription = desc; - } - } - - // Handle optional fields - if (fieldSchema instanceof z.ZodOptional) { - isRequired = false; - actualSchema = fieldSchema.unwrap(); - // If no description was found on the optional wrapper, try the unwrapped schema - if (!fieldDescription && actualSchema.description) { - fieldDescription = actualSchema.description; - } - } - - // Handle default values (they make fields optional) - if (fieldSchema instanceof z.ZodDefault) { - isRequired = false; - actualSchema = fieldSchema.removeDefault(); - // If no description was found on the default wrapper, try the unwrapped schema - if (!fieldDescription && actualSchema.description) { - fieldDescription = actualSchema.description; - } - } - - // Map Zod types to JSON Schema types - if (actualSchema instanceof z.ZodString) { - properties[key] = { - type: 'string', - description: fieldDescription || `${key} parameter`, - }; - } else if (actualSchema instanceof z.ZodEnum) { - properties[key] = { - type: 'string', - enum: actualSchema._def.values, - description: fieldDescription || `${key} parameter`, - }; - } else if (actualSchema instanceof z.ZodNumber) { - properties[key] = { - type: 'number', - description: fieldDescription || `${key} parameter`, - }; - } else if (actualSchema instanceof z.ZodBoolean) { - properties[key] = { - type: 'boolean', - description: fieldDescription || `${key} parameter`, - }; - } else if (actualSchema instanceof z.ZodArray) { - properties[key] = { - type: 'array', - items: { type: 'string' }, // Default to string array, can be enhanced - description: fieldDescription || `${key} parameter`, - }; - } else { - // Fallback for unknown types - properties[key] = { - type: 'string', - description: fieldDescription || `${key} parameter`, - }; - } - - if (isRequired) { - required.push(key); - } - }); - - return { - type: 'object', - properties, - required, - }; -} From f5f5c1c96e09ca93239e780dfd9b4df90985d0c8 Mon Sep 17 00:00:00 2001 From: e-roy Date: Fri, 5 Sep 2025 14:36:21 -0400 Subject: [PATCH 04/21] bump version --- packages/tools/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/package.json b/packages/tools/package.json index 9c09499..e120a96 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.2", + "version": "0.0.11-beta.3", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { From 6bb98549bb31a63105a2056dbd3faba54bc0a213 Mon Sep 17 00:00:00 2001 From: e-roy Date: Fri, 5 Sep 2025 17:09:24 -0400 Subject: [PATCH 05/21] testing with openai agents v0.1.0 --- apps/examples/openai/package.json | 4 +- apps/examples/vercel-ai/package.json | 2 +- packages/tools/package.json | 4 +- pnpm-lock.yaml | 454 +++++---------------------- 4 files changed, 90 insertions(+), 374 deletions(-) diff --git a/apps/examples/openai/package.json b/apps/examples/openai/package.json index d088e36..93517fb 100644 --- a/apps/examples/openai/package.json +++ b/apps/examples/openai/package.json @@ -9,9 +9,9 @@ "lint": "next lint" }, "dependencies": { - "@openai/agents": "^0.0.17", + "@openai/agents": "^0.1.0", "fmp-ai-tools": "workspace:*", - "next": "15.0.0", + "next": "15.3.0", "openai": "^4.63.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/apps/examples/vercel-ai/package.json b/apps/examples/vercel-ai/package.json index b2f638f..c8ac77c 100644 --- a/apps/examples/vercel-ai/package.json +++ b/apps/examples/vercel-ai/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "next": "15.0.0", + "next": "15.3.0", "react": "^19.0.0", "react-dom": "^19.0.0", "ai": "^5.0.5", diff --git a/packages/tools/package.json b/packages/tools/package.json index e120a96..1c27534 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.3", + "version": "0.0.11-beta.4", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { @@ -52,7 +52,7 @@ }, "homepage": "https://fmp-node-wrapper-docs.vercel.app", "dependencies": { - "@openai/agents": "^0.0.17", + "@openai/agents": "^0.1.0", "ai": "^5.0.5", "fmp-node-api": "workspace:*", "zod": "^3.25.76" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index add7a70..ec29f64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,13 +25,13 @@ importers: version: 9.32.0(jiti@2.4.2) jest: specifier: ^29.7.0 - version: 29.7.0 + version: 29.7.0(@types/node@20.19.2) rimraf: specifier: ^5.0.5 version: 5.0.10 ts-jest: specifier: ^29.1.2 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) turbo: specifier: ^2.5.5 version: 2.5.5 @@ -133,14 +133,14 @@ importers: apps/examples/openai: dependencies: '@openai/agents': - specifier: ^0.0.17 - version: 0.0.17(ws@8.18.3)(zod@3.25.76) + specifier: ^0.1.0 + version: 0.1.0(ws@8.18.3)(zod@3.25.76) fmp-ai-tools: specifier: workspace:* version: link:../../../packages/tools next: - specifier: 15.0.0 - version: 15.0.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.3.0 + version: 15.3.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) openai: specifier: ^4.63.0 version: 4.104.0(ws@8.18.3)(zod@3.25.76) @@ -197,8 +197,8 @@ importers: specifier: workspace:* version: link:../../../packages/tools next: - specifier: 15.0.0 - version: 15.0.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 15.3.0 + version: 15.3.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 @@ -271,8 +271,8 @@ importers: packages/tools: dependencies: '@openai/agents': - specifier: ^0.0.17 - version: 0.0.17(ws@8.18.3)(zod@3.25.76) + specifier: ^0.1.0 + version: 0.1.0(ws@8.18.3)(zod@3.25.76) ai: specifier: ^5.0.5 version: 5.0.5(zod@3.25.76) @@ -831,65 +831,33 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - '@img/sharp-darwin-arm64@0.34.2': resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - '@img/sharp-darwin-x64@0.34.2': resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} - cpu: [arm64] - os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.1.0': resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} - cpu: [x64] - os: [darwin] - '@img/sharp-libvips-darwin-x64@1.1.0': resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linux-arm64@1.1.0': resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} - cpu: [arm] - os: [linux] - '@img/sharp-libvips-linux-arm@1.1.0': resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} cpu: [arm] @@ -900,123 +868,62 @@ packages: cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} - cpu: [s390x] - os: [linux] - '@img/sharp-libvips-linux-s390x@1.1.0': resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linux-x64@1.1.0': resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} - cpu: [arm64] - os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} - cpu: [x64] - os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.1.0': resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linux-arm64@0.34.2': resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - '@img/sharp-linux-arm@0.34.2': resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - '@img/sharp-linux-s390x@0.34.2': resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linux-x64@0.34.2': resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.2': resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linuxmusl-x64@0.34.2': resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - '@img/sharp-wasm32@0.34.2': resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1028,24 +935,12 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - '@img/sharp-win32-ia32@0.34.2': resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@img/sharp-win32-x64@0.34.2': resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1182,8 +1077,8 @@ packages: '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} - '@next/env@15.0.0': - resolution: {integrity: sha512-Mcv8ZVmEgTO3bePiH/eJ7zHqQEs2gCqZ0UId2RxHmDDc7Pw6ngfSrOFlxG8XDpaex+n2G+TKPsQAf28MO+88Gw==} + '@next/env@15.3.0': + resolution: {integrity: sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA==} '@next/env@15.3.4': resolution: {integrity: sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==} @@ -1205,8 +1100,8 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@15.0.0': - resolution: {integrity: sha512-Gjgs3N7cFa40a9QT9AEHnuGKq69/bvIOn0SLGDV+ordq07QOP4k1GDOVedMHEjVeqy1HBLkL8rXnNTuMZIv79A==} + '@next/swc-darwin-arm64@15.3.0': + resolution: {integrity: sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1217,8 +1112,8 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.0.0': - resolution: {integrity: sha512-BUtTvY5u9s5berAuOEydAUlVMjnl6ZjXS+xVrMt317mglYZ2XXjY8YRDCaz9vYMjBNPXH8Gh75Cew5CMdVbWTw==} + '@next/swc-darwin-x64@15.3.0': + resolution: {integrity: sha512-m+eO21yg80En8HJ5c49AOQpFDq+nP51nu88ZOMCorvw3g//8g1JSUsEiPSiFpJo1KCTQ+jm9H0hwXK49H/RmXg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1229,8 +1124,8 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.0.0': - resolution: {integrity: sha512-sbCoEpuWUBpYoLSgYrk0CkBv8RFv4ZlPxbwqRHr/BWDBJppTBtF53EvsntlfzQJ9fosYX12xnS6ltxYYwsMBjg==} + '@next/swc-linux-arm64-gnu@15.3.0': + resolution: {integrity: sha512-H0Kk04ZNzb6Aq/G6e0un4B3HekPnyy6D+eUBYPJv9Abx8KDYgNMWzKt4Qhj57HXV3sTTjsfc1Trc1SxuhQB+Tg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1241,8 +1136,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.0.0': - resolution: {integrity: sha512-JAw84qfL81aQCirXKP4VkgmhiDpXJupGjt8ITUkHrOVlBd+3h5kjfPva5M0tH2F9KKSgJQHEo3F5S5tDH9h2ww==} + '@next/swc-linux-arm64-musl@15.3.0': + resolution: {integrity: sha512-k8GVkdMrh/+J9uIv/GpnHakzgDQhrprJ/FbGQvwWmstaeFG06nnAoZCJV+wO/bb603iKV1BXt4gHG+s2buJqZA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1253,8 +1148,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.0.0': - resolution: {integrity: sha512-r5Smd03PfxrGKMewdRf2RVNA1CU5l2rRlvZLQYZSv7FUsXD5bKEcOZ/6/98aqRwL7diXOwD8TCWJk1NbhATQHg==} + '@next/swc-linux-x64-gnu@15.3.0': + resolution: {integrity: sha512-ZMQ9yzDEts/vkpFLRAqfYO1wSpIJGlQNK9gZ09PgyjBJUmg8F/bb8fw2EXKgEaHbCc4gmqMpDfh+T07qUphp9A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1265,8 +1160,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.0.0': - resolution: {integrity: sha512-fM6qocafz4Xjhh79CuoQNeGPhDHGBBUbdVtgNFJOUM8Ih5ZpaDZlTvqvqsh5IoO06CGomxurEGqGz/4eR/FaMQ==} + '@next/swc-linux-x64-musl@15.3.0': + resolution: {integrity: sha512-RFwq5VKYTw9TMr4T3e5HRP6T4RiAzfDJ6XsxH8j/ZeYq2aLsBqCkFzwMI0FmnSsLaUbOb46Uov0VvN3UciHX5A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1277,8 +1172,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.0.0': - resolution: {integrity: sha512-ZOd7c/Lz1lv7qP/KzR513XEa7QzW5/P0AH3A5eR1+Z/KmDOvMucht0AozccPc0TqhdV1xaXmC0Fdx0hoNzk6ng==} + '@next/swc-win32-arm64-msvc@15.3.0': + resolution: {integrity: sha512-a7kUbqa/k09xPjfCl0RSVAvEjAkYBYxUzSVAzk2ptXiNEL+4bDBo9wNC43G/osLA/EOGzG4CuNRFnQyIHfkRgQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -1289,8 +1184,8 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.0.0': - resolution: {integrity: sha512-2RVWcLtsqg4LtaoJ3j7RoKpnWHgcrz5XvuUGE7vBYU2i6M2XeD9Y8RlLaF770LEIScrrl8MdWsp6odtC6sZccg==} + '@next/swc-win32-x64-msvc@15.3.0': + resolution: {integrity: sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1317,26 +1212,26 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} - '@openai/agents-core@0.0.17': - resolution: {integrity: sha512-+V8rtwq8OtvCvXIVN/8ZFJNfQXLeCpeaBuFwJY7QBa44lIdMSB3vULcUVm1L7bTGKeVKpdB8GDky64hOFG4w7Q==} + '@openai/agents-core@0.1.0': + resolution: {integrity: sha512-SASFdtW71/3Fmjl1gSCIIDTqeDkRQxU7H8SqpMFeB+lbXtnNFTxR5Wt6XnEdj++dRRY8x3EbRnAx8lT7CZGioA==} peerDependencies: zod: ^3.25.40 peerDependenciesMeta: zod: optional: true - '@openai/agents-openai@0.0.17': - resolution: {integrity: sha512-gGpcAWXh858HiNI51Nhl0MxM66asG0mv1LGASwB4jfzP7/GjM9k0on/ZlMBffM+6HID1zuDDYPbETD2y4XTY4w==} + '@openai/agents-openai@0.1.0': + resolution: {integrity: sha512-EdubPzCx4wj4YS07gX0mnpt1mHvDZXGfjDz+hFMOVbQHczIcLpv5gubRiMgFfALjhnCWVpOkeLCY/ikTY7YR0w==} peerDependencies: zod: ^3.25.40 - '@openai/agents-realtime@0.0.17': - resolution: {integrity: sha512-9yxe93XKuy/pMhFMSrqAu0GlB3mpHh6J9ZGv6PdVpgQyc8uTmujJakMTsYBlogWHJNmGlWIRgf0+x8jIVoTv4A==} + '@openai/agents-realtime@0.1.0': + resolution: {integrity: sha512-KCdAosaG3vy5WfZigiShsiV1HhqUuFc27BqYHaY5NkBecqvg8TnZMdgvXyxj/xVmw5csw43EXCc9t85Yugeatg==} peerDependencies: zod: ^3.25.40 - '@openai/agents@0.0.17': - resolution: {integrity: sha512-3tkN03WRLTTqcuzkopf37rAF6WtX0js3ciOqFn+EC1873J04Yf2m4xTYkYKT4RkwbOZh620+sIMPNKiUhW2rMg==} + '@openai/agents@0.1.0': + resolution: {integrity: sha512-SHuJOKvBkLi64+LaZ7JZibkKjtruZkVVKamZudwFZQ5Iw7yvR7XjRnAg8CTbZ9OePjoq3sfa/PnA4V7EjaZo6A==} peerDependencies: zod: ^3.25.40 @@ -1487,9 +1382,6 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.13': - resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -3867,16 +3759,16 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.0.0: - resolution: {integrity: sha512-/ivqF6gCShXpKwY9hfrIQYh8YMge8L3W+w1oRLv/POmK4MOQnh+FscZ8a0fRFTSQWE+2z9ctNYvELD9vP2FV+A==} - engines: {node: '>=18.18.0'} + next@15.3.0: + resolution: {integrity: sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-65a56d0e-20241020 - react-dom: ^18.2.0 || 19.0.0-rc-65a56d0e-20241020 + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': @@ -4000,8 +3892,8 @@ packages: zod: optional: true - openai@5.12.0: - resolution: {integrity: sha512-vUdt02xiWgOHiYUmW0Hj1Qu9OKAiVQu5Bd547ktVCiMKC1BkB5L3ImeEnCyq3WpRKR6ZTaPgekzqdozwdPs7Lg==} + openai@5.19.1: + resolution: {integrity: sha512-zSqnUF7oR9ksmpusKkpUgkNrj8Sl57U+OyzO8jzc7LUjTMg4DRfR3uCm+EIMA6iw06sRPNp4t7ojp3sCpEUZRQ==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -4472,10 +4364,6 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.34.2: resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -5651,142 +5539,73 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/sharp-darwin-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 - optional: true - '@img/sharp-darwin-arm64@0.34.2': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.1.0 optional: true - '@img/sharp-darwin-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - optional: true - '@img/sharp-darwin-x64@0.34.2': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.1.0 optional: true - '@img/sharp-libvips-darwin-arm64@1.0.4': - optional: true - '@img/sharp-libvips-darwin-arm64@1.1.0': optional: true - '@img/sharp-libvips-darwin-x64@1.0.4': - optional: true - '@img/sharp-libvips-darwin-x64@1.1.0': optional: true - '@img/sharp-libvips-linux-arm64@1.0.4': - optional: true - '@img/sharp-libvips-linux-arm64@1.1.0': optional: true - '@img/sharp-libvips-linux-arm@1.0.5': - optional: true - '@img/sharp-libvips-linux-arm@1.1.0': optional: true '@img/sharp-libvips-linux-ppc64@1.1.0': optional: true - '@img/sharp-libvips-linux-s390x@1.0.4': - optional: true - '@img/sharp-libvips-linux-s390x@1.1.0': optional: true - '@img/sharp-libvips-linux-x64@1.0.4': - optional: true - '@img/sharp-libvips-linux-x64@1.1.0': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - optional: true - '@img/sharp-libvips-linuxmusl-x64@1.1.0': optional: true - '@img/sharp-linux-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 - optional: true - '@img/sharp-linux-arm64@0.34.2': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.1.0 optional: true - '@img/sharp-linux-arm@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 - optional: true - '@img/sharp-linux-arm@0.34.2': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.1.0 optional: true - '@img/sharp-linux-s390x@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 - optional: true - '@img/sharp-linux-s390x@0.34.2': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.1.0 optional: true - '@img/sharp-linux-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - optional: true - '@img/sharp-linux-x64@0.34.2': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.1.0 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-arm64@0.34.2': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-x64@0.34.2': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.1.0 optional: true - '@img/sharp-wasm32@0.33.5': - dependencies: - '@emnapi/runtime': 1.4.3 - optional: true - '@img/sharp-wasm32@0.34.2': dependencies: '@emnapi/runtime': 1.4.3 @@ -5795,15 +5614,9 @@ snapshots: '@img/sharp-win32-arm64@0.34.2': optional: true - '@img/sharp-win32-ia32@0.33.5': - optional: true - '@img/sharp-win32-ia32@0.34.2': optional: true - '@img/sharp-win32-x64@0.33.5': - optional: true - '@img/sharp-win32-x64@0.34.2': optional: true @@ -6094,7 +5907,7 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@next/env@15.0.0': {} + '@next/env@15.3.0': {} '@next/env@15.3.4': {} @@ -6113,49 +5926,49 @@ snapshots: '@mdx-js/loader': 3.1.0(acorn@8.15.0) '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.1.0) - '@next/swc-darwin-arm64@15.0.0': + '@next/swc-darwin-arm64@15.3.0': optional: true '@next/swc-darwin-arm64@15.3.4': optional: true - '@next/swc-darwin-x64@15.0.0': + '@next/swc-darwin-x64@15.3.0': optional: true '@next/swc-darwin-x64@15.3.4': optional: true - '@next/swc-linux-arm64-gnu@15.0.0': + '@next/swc-linux-arm64-gnu@15.3.0': optional: true '@next/swc-linux-arm64-gnu@15.3.4': optional: true - '@next/swc-linux-arm64-musl@15.0.0': + '@next/swc-linux-arm64-musl@15.3.0': optional: true '@next/swc-linux-arm64-musl@15.3.4': optional: true - '@next/swc-linux-x64-gnu@15.0.0': + '@next/swc-linux-x64-gnu@15.3.0': optional: true '@next/swc-linux-x64-gnu@15.3.4': optional: true - '@next/swc-linux-x64-musl@15.0.0': + '@next/swc-linux-x64-musl@15.3.0': optional: true '@next/swc-linux-x64-musl@15.3.4': optional: true - '@next/swc-win32-arm64-msvc@15.0.0': + '@next/swc-win32-arm64-msvc@15.3.0': optional: true '@next/swc-win32-arm64-msvc@15.3.4': optional: true - '@next/swc-win32-x64-msvc@15.0.0': + '@next/swc-win32-x64-msvc@15.3.0': optional: true '@next/swc-win32-x64-msvc@15.3.4': @@ -6175,10 +5988,10 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@openai/agents-core@0.0.17(ws@8.18.3)(zod@3.25.76)': + '@openai/agents-core@0.1.0(ws@8.18.3)(zod@3.25.76)': dependencies: debug: 4.4.1 - openai: 5.12.0(ws@8.18.3)(zod@3.25.76) + openai: 5.19.1(ws@8.18.3)(zod@3.25.76) optionalDependencies: '@modelcontextprotocol/sdk': 1.17.4 zod: 3.25.76 @@ -6186,19 +5999,19 @@ snapshots: - supports-color - ws - '@openai/agents-openai@0.0.17(ws@8.18.3)(zod@3.25.76)': + '@openai/agents-openai@0.1.0(ws@8.18.3)(zod@3.25.76)': dependencies: - '@openai/agents-core': 0.0.17(ws@8.18.3)(zod@3.25.76) + '@openai/agents-core': 0.1.0(ws@8.18.3)(zod@3.25.76) debug: 4.4.1 - openai: 5.12.0(ws@8.18.3)(zod@3.25.76) + openai: 5.19.1(ws@8.18.3)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - supports-color - ws - '@openai/agents-realtime@0.0.17(zod@3.25.76)': + '@openai/agents-realtime@0.1.0(zod@3.25.76)': dependencies: - '@openai/agents-core': 0.0.17(ws@8.18.3)(zod@3.25.76) + '@openai/agents-core': 0.1.0(ws@8.18.3)(zod@3.25.76) '@types/ws': 8.18.1 debug: 4.4.1 ws: 8.18.3 @@ -6208,13 +6021,13 @@ snapshots: - supports-color - utf-8-validate - '@openai/agents@0.0.17(ws@8.18.3)(zod@3.25.76)': + '@openai/agents@0.1.0(ws@8.18.3)(zod@3.25.76)': dependencies: - '@openai/agents-core': 0.0.17(ws@8.18.3)(zod@3.25.76) - '@openai/agents-openai': 0.0.17(ws@8.18.3)(zod@3.25.76) - '@openai/agents-realtime': 0.0.17(zod@3.25.76) + '@openai/agents-core': 0.1.0(ws@8.18.3)(zod@3.25.76) + '@openai/agents-openai': 0.1.0(ws@8.18.3)(zod@3.25.76) + '@openai/agents-realtime': 0.1.0(zod@3.25.76) debug: 4.4.1 - openai: 5.12.0(ws@8.18.3)(zod@3.25.76) + openai: 5.19.1(ws@8.18.3)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -6318,10 +6131,6 @@ snapshots: '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.13': - dependencies: - tslib: 2.8.1 - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -7189,21 +6998,6 @@ snapshots: vary: 1.1.2 optional: true - create-jest@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.19.1) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - create-jest@29.7.0(@types/node@20.19.1): dependencies: '@jest/types': 29.6.3 @@ -7555,8 +7349,8 @@ snapshots: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.32.0(jiti@2.4.2)) @@ -7590,7 +7384,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -7601,7 +7395,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -7616,14 +7410,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -7656,7 +7450,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7667,7 +7461,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -8677,25 +8471,6 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0: - dependencies: - '@jest/core': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0 - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.1) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest-cli@29.7.0(@types/node@20.19.1): dependencies: '@jest/core': 29.7.0 @@ -9009,18 +8784,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0: - dependencies: - '@jest/core': 29.7.0 - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest@29.7.0(@types/node@20.19.1): dependencies: '@jest/core': 29.7.0 @@ -9729,11 +9492,11 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - next@15.0.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.3.0(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 15.0.0 + '@next/env': 15.3.0 '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.15 busboy: 1.6.0 caniuse-lite: 1.0.30001726 postcss: 8.4.31 @@ -9741,16 +9504,16 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(@babel/core@7.27.7)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.0.0 - '@next/swc-darwin-x64': 15.0.0 - '@next/swc-linux-arm64-gnu': 15.0.0 - '@next/swc-linux-arm64-musl': 15.0.0 - '@next/swc-linux-x64-gnu': 15.0.0 - '@next/swc-linux-x64-musl': 15.0.0 - '@next/swc-win32-arm64-msvc': 15.0.0 - '@next/swc-win32-x64-msvc': 15.0.0 + '@next/swc-darwin-arm64': 15.3.0 + '@next/swc-darwin-x64': 15.3.0 + '@next/swc-linux-arm64-gnu': 15.3.0 + '@next/swc-linux-arm64-musl': 15.3.0 + '@next/swc-linux-x64-gnu': 15.3.0 + '@next/swc-linux-x64-musl': 15.3.0 + '@next/swc-win32-arm64-msvc': 15.3.0 + '@next/swc-win32-x64-msvc': 15.3.0 '@opentelemetry/api': 1.9.0 - sharp: 0.33.5 + sharp: 0.34.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -9871,7 +9634,7 @@ snapshots: transitivePeerDependencies: - encoding - openai@5.12.0(ws@8.18.3)(zod@3.25.76): + openai@5.19.1(ws@8.18.3)(zod@3.25.76): optionalDependencies: ws: 8.18.3 zod: 3.25.76 @@ -10430,33 +10193,6 @@ snapshots: setprototypeof@1.2.0: optional: true - sharp@0.33.5: - dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 - optional: true - sharp@0.34.2: dependencies: color: 4.2.3 @@ -10848,26 +10584,6 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.27.7) jest-util: 29.7.0 - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.8.3): - dependencies: - bs-logger: 0.2.6 - ejs: 3.1.10 - fast-json-stable-stringify: 2.1.0 - jest: 29.7.0 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.2 - type-fest: 4.41.0 - typescript: 5.8.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.27.7 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.27.7) - jest-util: 29.7.0 - tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 From f873415983087869186dd243b60a57ec789800ed Mon Sep 17 00:00:00 2001 From: e-roy Date: Mon, 8 Sep 2025 08:44:04 -0400 Subject: [PATCH 06/21] fix: version check for openai agents and it's testing --- packages/tools/package.json | 2 +- .../__tests__/providers/openai/index.test.ts | 7 ++ .../utils/openai-tool-wrapper.test.ts | 38 +++++- .../src/__tests__/utils/version-check.test.ts | 119 +++++++++--------- packages/tools/src/utils/version-check.ts | 91 ++++++++++---- 5 files changed, 168 insertions(+), 89 deletions(-) diff --git a/packages/tools/package.json b/packages/tools/package.json index 1c27534..b4d8808 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.4", + "version": "0.0.11-beta.5", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/__tests__/providers/openai/index.test.ts b/packages/tools/src/__tests__/providers/openai/index.test.ts index 84d22ed..681a581 100644 --- a/packages/tools/src/__tests__/providers/openai/index.test.ts +++ b/packages/tools/src/__tests__/providers/openai/index.test.ts @@ -1,4 +1,11 @@ import type { Tool } from '@openai/agents'; + +// Mock the version check to prevent it from running during import +jest.mock('@/utils/version-check', () => ({ + checkOpenAIAgentsVersion: jest.fn(), + warnOpenAIAgentsVersion: jest.fn(), +})); + import * as OpenAIProviders from '@/providers/openai'; describe('OpenAI providers index exports', () => { diff --git a/packages/tools/src/__tests__/utils/openai-tool-wrapper.test.ts b/packages/tools/src/__tests__/utils/openai-tool-wrapper.test.ts index 380a031..20022cc 100644 --- a/packages/tools/src/__tests__/utils/openai-tool-wrapper.test.ts +++ b/packages/tools/src/__tests__/utils/openai-tool-wrapper.test.ts @@ -20,8 +20,29 @@ describe('createOpenAITool', () => { execute: async () => 'ok', }); - // With the new API, parameters contains the Zod schema directly - expect(tool.parameters).toBe(schema); + // The parameters should be a JSON schema object + expect(tool.parameters).toEqual({ + type: 'object', + properties: { + aString: { type: 'string' }, + aNumber: { type: 'string' }, + aBoolean: { type: 'string' }, + anEnum: { type: 'string' }, + anArray: { type: 'string' }, + optionalField: { type: 'string' }, + defaultField: { type: 'string' }, + }, + required: [ + 'aString', + 'aNumber', + 'aBoolean', + 'anEnum', + 'anArray', + 'optionalField', + 'defaultField', + ], + additionalProperties: false, + }); // Test that the tool has the expected properties expect(tool.name).toBe('testTool'); @@ -94,8 +115,17 @@ describe('createOpenAITool', () => { execute: async () => 'ok', }); - // With the new API, parameters contains the Zod schema directly - expect(tool.parameters).toBe(schema); + // The parameters should be a JSON schema object + expect(tool.parameters).toEqual({ + type: 'object', + properties: { + optInner: { type: 'string' }, + defInner: { type: 'string' }, + unknown: { type: 'string' }, + }, + required: ['optInner', 'defInner', 'unknown'], + additionalProperties: false, + }); // Test that the tool has the expected properties expect(tool.name).toBe('branchTool'); diff --git a/packages/tools/src/__tests__/utils/version-check.test.ts b/packages/tools/src/__tests__/utils/version-check.test.ts index 35cf502..921a6f0 100644 --- a/packages/tools/src/__tests__/utils/version-check.test.ts +++ b/packages/tools/src/__tests__/utils/version-check.test.ts @@ -2,97 +2,90 @@ const mockConsoleWarn = jest.fn(); const originalConsoleWarn = console.warn; -// Mock the @openai/agents module -jest.mock('@openai/agents', () => ({ - tool: jest.fn(), -})); - describe('Version Check Utility', () => { - let mockTool: jest.MockedFunction; - let versionCheckModule: any; - - beforeEach(async () => { + beforeEach(() => { jest.clearAllMocks(); mockConsoleWarn.mockClear(); console.warn = mockConsoleWarn; - - // Get the mocked tool function - const openaiAgents = await import('@openai/agents'); - mockTool = openaiAgents.tool; - - // Import the version check module - versionCheckModule = await import('../../utils/version-check'); }); afterAll(() => { console.warn = originalConsoleWarn; }); - describe('checkOpenAIAgentsVersion', () => { - it('should pass when @openai/agents is compatible', () => { - mockTool.mockReturnValue({}); + describe('warnOpenAIAgentsVersion', () => { + it('should handle errors gracefully', async () => { + const { warnOpenAIAgentsVersion } = await import('../../utils/version-check'); - expect(() => versionCheckModule.checkOpenAIAgentsVersion()).not.toThrow(); + // This should not throw, even if there are issues + expect(() => warnOpenAIAgentsVersion()).not.toThrow(); }); + }); - it('should throw error when @openai/agents is incompatible', () => { - mockTool.mockImplementation(() => { - throw new Error('Zod field uses .optional() without .nullable()'); - }); + describe('error handling', () => { + it('should handle missing package gracefully in warn function', async () => { + const { warnOpenAIAgentsVersion } = await import('../../utils/version-check'); - expect(() => versionCheckModule.checkOpenAIAgentsVersion()).toThrow( - 'Incompatible @openai/agents version detected', - ); - expect(() => versionCheckModule.checkOpenAIAgentsVersion()).toThrow( - 'This package requires version ^0.0.17 or higher', - ); - expect(() => versionCheckModule.checkOpenAIAgentsVersion()).toThrow( - 'npm install @openai/agents@latest', + // Mock require.resolve to throw an error + const originalResolve = require.resolve; + require.resolve = jest.fn().mockImplementation(() => { + throw new Error('Cannot resolve module'); + }) as unknown as typeof require.resolve; + + warnOpenAIAgentsVersion(); + expect(mockConsoleWarn).toHaveBeenCalledWith( + '⚠️ Version compatibility warning:', + expect.stringContaining('@openai/agents package not found'), ); + + // Restore original function + require.resolve = originalResolve; }); + }); - it('should include error details in thrown message', () => { - const testError = new Error('Test error message'); - mockTool.mockImplementation(() => { - throw testError; - }); + describe('error messages', () => { + it('should provide helpful error messages', async () => { + const { checkOpenAIAgentsVersion } = await import('../../utils/version-check'); - expect(() => versionCheckModule.checkOpenAIAgentsVersion()).toThrow( - 'Error details: Test error message', - ); + const originalResolve = require.resolve; + require.resolve = jest.fn().mockImplementation(() => { + throw new Error('Cannot resolve module'); + }) as unknown as typeof require.resolve; + + expect(() => checkOpenAIAgentsVersion()).toThrow('npm install @openai/agents@0.1.0'); + expect(() => checkOpenAIAgentsVersion()).toThrow('@openai/agents package not found'); + + require.resolve = originalResolve; }); }); - describe('warnOpenAIAgentsVersion', () => { - it('should not warn when version is compatible', () => { - mockTool.mockReturnValue({}); + describe('error scenarios', () => { + it('should handle missing package gracefully', async () => { + const { warnOpenAIAgentsVersion } = await import('../../utils/version-check'); - versionCheckModule.warnOpenAIAgentsVersion(); - expect(mockConsoleWarn).not.toHaveBeenCalled(); + // Test that warn function handles errors gracefully + expect(() => warnOpenAIAgentsVersion()).not.toThrow(); }); - it('should warn when version is incompatible', () => { - mockTool.mockImplementation(() => { - throw new Error('Incompatible version'); - }); + it('should provide helpful error messages', async () => { + const { checkOpenAIAgentsVersion } = await import('../../utils/version-check'); - versionCheckModule.warnOpenAIAgentsVersion(); - expect(mockConsoleWarn).toHaveBeenCalledWith( - '⚠️ Version compatibility warning:', - 'Incompatible @openai/agents version detected. This package requires version ^0.0.17 or higher due to breaking changes in the API. Please upgrade with: npm install @openai/agents@latest\n\nError details: Incompatible version', - ); + // Test that error messages contain helpful information + try { + checkOpenAIAgentsVersion(); + } catch (error) { + expect((error as Error).message).toContain('@openai/agents package not found'); + expect((error as Error).message).toContain('npm install @openai/agents@0.1.0'); + } }); + }); - it('should handle non-Error objects thrown', () => { - mockTool.mockImplementation(() => { - throw 'String error'; - }); + describe('integration', () => { + it('should work with actual installed version', async () => { + const { warnOpenAIAgentsVersion } = await import('../../utils/version-check'); - versionCheckModule.warnOpenAIAgentsVersion(); - expect(mockConsoleWarn).toHaveBeenCalledWith( - '⚠️ Version compatibility warning:', - 'Incompatible @openai/agents version detected. This package requires version ^0.0.17 or higher due to breaking changes in the API. Please upgrade with: npm install @openai/agents@latest\n\nError details: String error', - ); + // Test that warn function works with real environment + expect(() => warnOpenAIAgentsVersion()).not.toThrow(); }); }); }); diff --git a/packages/tools/src/utils/version-check.ts b/packages/tools/src/utils/version-check.ts index 83b07c3..bbd5b41 100644 --- a/packages/tools/src/utils/version-check.ts +++ b/packages/tools/src/utils/version-check.ts @@ -1,31 +1,80 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const REQUIRED_VERSION = '0.1.0'; + +/** + * Gets the actual installed version of a package + */ +function getInstalledPackageVersion(packageName: string): string | null { + try { + const packagePath = path.dirname(require.resolve(packageName)); + const packageJsonPath = path.join(packagePath, '..', 'package.json'); + + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return packageJson.version; + } + } catch (_error) { + // Package not found or other error + return null; + } + return null; +} + +/** + * Compares two semantic versions + * Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2 + */ +function compareVersions(v1: string, v2: string): number { + const parseVersion = (version: string): number[] => { + return version.split('.').map(num => parseInt(num, 10) || 0); + }; + + const v1Parts = parseVersion(v1); + const v2Parts = parseVersion(v2); + + const maxLength = Math.max(v1Parts.length, v2Parts.length); + + for (let i = 0; i < maxLength; i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; + + if (v1Part < v2Part) return -1; + if (v1Part > v2Part) return 1; + } + + return 0; +} + +/** + * Checks if a version exactly matches the required version + */ +function matchesExactVersion(installedVersion: string, requiredVersion: string): boolean { + return compareVersions(installedVersion, requiredVersion) === 0; +} + /** * Checks if the installed version of @openai/agents is compatible * with this package. Throws an error if incompatible. */ export function checkOpenAIAgentsVersion(): void { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { tool } = require('@openai/agents'); - - // Test if the tool function accepts the new API structure - // This is a runtime check that will fail with older versions - tool({ - name: 'test', - description: 'test', - parameters: { type: 'object', properties: {} }, - strict: true, - execute: async () => 'test', - }); - - // If we get here, the version is compatible (silent success) - } catch (error) { - // If the tool creation fails, it's likely an incompatible version - const errorMessage = error instanceof Error ? error.message : String(error); + const installedVersion = getInstalledPackageVersion('@openai/agents'); + + if (!installedVersion) { + throw new Error( + `@openai/agents package not found. ` + + `This package requires @openai/agents to be installed. ` + + `Please install with: npm install @openai/agents@${REQUIRED_VERSION}`, + ); + } + + if (!matchesExactVersion(installedVersion, REQUIRED_VERSION)) { throw new Error( `Incompatible @openai/agents version detected. ` + - `This package requires version ^0.0.17 or higher due to breaking changes in the API. ` + - `Please upgrade with: npm install @openai/agents@latest\n\n` + - `Error details: ${errorMessage}`, + `Installed version: ${installedVersion}, Required: ${REQUIRED_VERSION} ` + + `This package requires exactly version ${REQUIRED_VERSION} due to API compatibility. ` + + `Please install with: npm install @openai/agents@${REQUIRED_VERSION}`, ); } } From 74bb483d0e2ef5d5f79fb4fd4195c7618be9ee51 Mon Sep 17 00:00:00 2001 From: e-roy Date: Mon, 8 Sep 2025 16:36:35 -0400 Subject: [PATCH 07/21] fix: remove any type from aisdk wrapper --- apps/examples/openai/next-env.d.ts | 2 +- apps/examples/vercel-ai/next-env.d.ts | 2 +- apps/examples/vercel-ai/package.json | 2 +- .../tools/src/providers/vercel-ai/index.ts | 4 - .../tools/src/utils/aisdk-tool-wrapper.ts | 19 ++-- pnpm-lock.yaml | 100 +++++++++++++++--- 6 files changed, 99 insertions(+), 30 deletions(-) diff --git a/apps/examples/openai/next-env.d.ts b/apps/examples/openai/next-env.d.ts index 40c3d68..1b3be08 100644 --- a/apps/examples/openai/next-env.d.ts +++ b/apps/examples/openai/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/examples/vercel-ai/next-env.d.ts b/apps/examples/vercel-ai/next-env.d.ts index 40c3d68..1b3be08 100644 --- a/apps/examples/vercel-ai/next-env.d.ts +++ b/apps/examples/vercel-ai/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/examples/vercel-ai/package.json b/apps/examples/vercel-ai/package.json index c8ac77c..2834e32 100644 --- a/apps/examples/vercel-ai/package.json +++ b/apps/examples/vercel-ai/package.json @@ -16,7 +16,7 @@ "@ai-sdk/openai": "^2.0.3", "@ai-sdk/react": "^2.0.3", "fmp-ai-tools": "workspace:*", - "zod": "^3.22.4" + "zod": "^3.25.76" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/packages/tools/src/providers/vercel-ai/index.ts b/packages/tools/src/providers/vercel-ai/index.ts index 2a7a08c..2f310bb 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -1,4 +1,3 @@ -/* istanbul ignore file */ import { quoteTools } from './quote'; import { companyTools } from './company'; import { financialTools } from './financial'; @@ -72,6 +71,3 @@ export { senateHouseTools, stockTools, }; - -// Re-export types -export type { ToolSet } from 'ai'; diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index 46a4b78..ad98f03 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -1,5 +1,4 @@ import { z } from 'zod'; -import { tool } from 'ai'; import { logApiExecutionWithTiming } from './logger'; // Tool configuration interface for AI SDK v2 @@ -10,17 +9,25 @@ export interface ToolConfig { execute: (args: z.infer) => Promise; } -// AI SDK v2 compatible tool creator using the ai library's tool function -export function createTool(config: ToolConfig) { +// Simple tool interface that's compatible with AI SDK ToolSet +export interface SimpleTool { + name: string; + description: string; + inputSchema: z.ZodType; + execute: (args: any) => Promise; +} + +// AI SDK v2 compatible tool creator that returns a simple tool object +// This avoids the complex type inference issues with the AI SDK's Tool type +export function createTool(config: ToolConfig): SimpleTool { const { name, description, inputSchema, execute } = config; - // Use the AI SDK's tool function to create a compatible tool - return tool({ + return { name, description, inputSchema, execute: async (args: z.infer) => { return await logApiExecutionWithTiming(name, args, () => execute(args)); }, - } as any); // Use type assertion to avoid deep type inference issues + }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec29f64..00d0874 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,13 +25,13 @@ importers: version: 9.32.0(jiti@2.4.2) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.2) + version: 29.7.0 rimraf: specifier: ^5.0.5 version: 5.0.10 ts-jest: specifier: ^29.1.2 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.8.3) turbo: specifier: ^2.5.5 version: 2.5.5 @@ -206,7 +206,7 @@ importers: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) zod: - specifier: ^3.22.4 + specifier: ^3.25.76 version: 3.25.76 devDependencies: '@types/node': @@ -257,7 +257,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.2 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -309,7 +309,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.0 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -6998,6 +6998,21 @@ snapshots: vary: 1.1.2 optional: true + create-jest@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.1) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@20.19.1): dependencies: '@jest/types': 29.6.3 @@ -7349,8 +7364,8 @@ snapshots: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.32.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.32.0(jiti@2.4.2)) @@ -7384,7 +7399,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -7395,7 +7410,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -7410,14 +7425,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -7450,7 +7465,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7461,7 +7476,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.32.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.4.2)))(eslint@9.32.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -8471,6 +8486,25 @@ snapshots: - babel-plugin-macros - supports-color + jest-cli@29.7.0: + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0 + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.1) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@20.19.1): dependencies: '@jest/core': 29.7.0 @@ -8784,6 +8818,18 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@29.7.0: + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@20.19.1): dependencies: '@jest/core': 29.7.0 @@ -10543,12 +10589,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.2) + jest: 29.7.0(@types/node@20.19.1) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -10564,12 +10610,32 @@ snapshots: esbuild: 0.25.5 jest-util: 29.7.0 - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.1) + jest: 29.7.0(@types/node@20.19.2) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.27.7 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.7) + jest-util: 29.7.0 + + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.8.3): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 From d9478bc50f38125e6ad6a916fbee472323958c9c Mon Sep 17 00:00:00 2001 From: e-roy Date: Tue, 9 Sep 2025 15:22:38 -0400 Subject: [PATCH 08/21] test release without logging function --- .../tools/src/utils/aisdk-tool-wrapper.ts | 61 ++++++++++++------- .../tools/src/utils/openai-tool-wrapper.ts | 28 +++++---- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index ad98f03..0dfef4e 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -1,33 +1,50 @@ import { z } from 'zod'; -import { logApiExecutionWithTiming } from './logger'; +import { tool } from 'ai'; +// import { logApiExecutionWithTiming } from './logger'; -// Tool configuration interface for AI SDK v2 -export interface ToolConfig { - name: string; - description: string; - inputSchema: T; - execute: (args: z.infer) => Promise; -} +// // Tool configuration interface for AI SDK v2 +// export interface ToolConfig { +// name: string; +// description: string; +// inputSchema: T; +// execute: (args: z.infer) => Promise; +// } + +// // AI SDK v2 compatible tool creator using the ai library's tool function +// // This properly converts Zod schemas to JSON Schema format +// export function createTool(config: ToolConfig) { +// const { name, description, inputSchema, execute } = config; -// Simple tool interface that's compatible with AI SDK ToolSet -export interface SimpleTool { +// // Use the AI SDK's tool function which properly handles Zod to JSON Schema conversion +// // Use type assertion to bypass complex type inference issues +// return tool({ +// description, +// parameters: inputSchema, +// execute: async (args: z.infer) => { +// return await logApiExecutionWithTiming(name, args, () => execute(args)); +// }, +// } as any); +// } + +interface ToolConfig { name: string; description: string; - inputSchema: z.ZodType; - execute: (args: any) => Promise; + inputSchema: z.ZodSchema; + execute: (input: any) => Promise; } -// AI SDK v2 compatible tool creator that returns a simple tool object -// This avoids the complex type inference issues with the AI SDK's Tool type -export function createTool(config: ToolConfig): SimpleTool { +export const createTool = (config: ToolConfig) => { const { name, description, inputSchema, execute } = config; - - return { - name, + return tool({ + // name, description, inputSchema, - execute: async (args: z.infer) => { - return await logApiExecutionWithTiming(name, args, () => execute(args)); + execute: async (input: any) => { + console.log(`🔧 Tool: ${name}`); + const result = await execute(input); + console.log(`🔧 Tool Result: ${name}`, result); + return result; + // return await logApiExecutionWithTiming(name, input, () => execute(input)); }, - }; -} + } as any); +}; diff --git a/packages/tools/src/utils/openai-tool-wrapper.ts b/packages/tools/src/utils/openai-tool-wrapper.ts index 659c53a..5ecff73 100644 --- a/packages/tools/src/utils/openai-tool-wrapper.ts +++ b/packages/tools/src/utils/openai-tool-wrapper.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { tool } from '@openai/agents'; -import { logApiExecutionWithTiming } from './logger'; +// import { logApiExecutionWithTiming } from './logger'; export interface OpenAIToolConfig> { name: string; @@ -29,18 +29,22 @@ export function createOpenAITool>(config: OpenAIToolC properties, required, additionalProperties: false, - }, + } as any, strict: true, - execute: async (input: unknown) => { - try { - const validatedInput = inputSchema.parse(input); - return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); - } catch (error) { - if (error instanceof z.ZodError) { - return `Invalid input: ${error.errors.map(e => e.message).join(', ')}`; - } - return `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`; - } + execute: async (args: z.TypeOf) => { + console.log(`🔧 Tool: ${name}`); + const result = await execute(args); + console.log(`🔧 Tool Result: ${name}`, result); + return result; + // try { + // const validatedInput = inputSchema.parse(input); + // return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); + // } catch (error) { + // if (error instanceof z.ZodError) { + // return `Invalid input: ${error.errors.map(e => e.message).join(', ')}`; + // } + // return `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`; + // } }, }); } From f0119e577551d6f88e10ef661ae8a3cbaef5ab65 Mon Sep 17 00:00:00 2001 From: e-roy Date: Tue, 9 Sep 2025 15:49:30 -0400 Subject: [PATCH 09/21] remove error throw on different version, brings back logging function for openai --- .../tools/src/utils/openai-tool-wrapper.ts | 24 ++++++++----------- packages/tools/src/utils/version-check.ts | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/tools/src/utils/openai-tool-wrapper.ts b/packages/tools/src/utils/openai-tool-wrapper.ts index 5ecff73..ac0fc7f 100644 --- a/packages/tools/src/utils/openai-tool-wrapper.ts +++ b/packages/tools/src/utils/openai-tool-wrapper.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { tool } from '@openai/agents'; -// import { logApiExecutionWithTiming } from './logger'; +import { logApiExecutionWithTiming } from './logger'; export interface OpenAIToolConfig> { name: string; @@ -32,19 +32,15 @@ export function createOpenAITool>(config: OpenAIToolC } as any, strict: true, execute: async (args: z.TypeOf) => { - console.log(`🔧 Tool: ${name}`); - const result = await execute(args); - console.log(`🔧 Tool Result: ${name}`, result); - return result; - // try { - // const validatedInput = inputSchema.parse(input); - // return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); - // } catch (error) { - // if (error instanceof z.ZodError) { - // return `Invalid input: ${error.errors.map(e => e.message).join(', ')}`; - // } - // return `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`; - // } + try { + const validatedInput = inputSchema.parse(args); + return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); + } catch (error) { + if (error instanceof z.ZodError) { + return `Invalid input: ${error.errors.map(e => e.message).join(', ')}`; + } + return `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`; + } }, }); } diff --git a/packages/tools/src/utils/version-check.ts b/packages/tools/src/utils/version-check.ts index bbd5b41..7423bea 100644 --- a/packages/tools/src/utils/version-check.ts +++ b/packages/tools/src/utils/version-check.ts @@ -70,7 +70,7 @@ export function checkOpenAIAgentsVersion(): void { } if (!matchesExactVersion(installedVersion, REQUIRED_VERSION)) { - throw new Error( + console.warn( `Incompatible @openai/agents version detected. ` + `Installed version: ${installedVersion}, Required: ${REQUIRED_VERSION} ` + `This package requires exactly version ${REQUIRED_VERSION} due to API compatibility. ` + From 7119c723bff3324b239a6def242248af2e498ac0 Mon Sep 17 00:00:00 2001 From: e-roy Date: Tue, 9 Sep 2025 15:50:02 -0400 Subject: [PATCH 10/21] bump beta version --- packages/tools/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/package.json b/packages/tools/package.json index b4d8808..31509c8 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.5", + "version": "0.0.11-beta.6", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { From 6b241b4ee09ab944879bd68b81deab5d930f5543 Mon Sep 17 00:00:00 2001 From: e-roy Date: Tue, 9 Sep 2025 16:23:22 -0400 Subject: [PATCH 11/21] add back logging function for testing in ai sdk --- packages/tools/package.json | 2 +- .../tools/src/utils/aisdk-tool-wrapper.ts | 74 +++++++++---------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/tools/package.json b/packages/tools/package.json index 31509c8..e96820d 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.6", + "version": "0.0.11-beta.7", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index 0dfef4e..2261e36 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -1,50 +1,50 @@ import { z } from 'zod'; import { tool } from 'ai'; -// import { logApiExecutionWithTiming } from './logger'; +import { logApiExecutionWithTiming } from './logger'; -// // Tool configuration interface for AI SDK v2 -// export interface ToolConfig { -// name: string; -// description: string; -// inputSchema: T; -// execute: (args: z.infer) => Promise; -// } - -// // AI SDK v2 compatible tool creator using the ai library's tool function -// // This properly converts Zod schemas to JSON Schema format -// export function createTool(config: ToolConfig) { -// const { name, description, inputSchema, execute } = config; - -// // Use the AI SDK's tool function which properly handles Zod to JSON Schema conversion -// // Use type assertion to bypass complex type inference issues -// return tool({ -// description, -// parameters: inputSchema, -// execute: async (args: z.infer) => { -// return await logApiExecutionWithTiming(name, args, () => execute(args)); -// }, -// } as any); -// } - -interface ToolConfig { +// Tool configuration interface for AI SDK v2 +export interface ToolConfig { name: string; description: string; - inputSchema: z.ZodSchema; - execute: (input: any) => Promise; + inputSchema: T; + execute: (args: z.infer) => Promise; } -export const createTool = (config: ToolConfig) => { +// AI SDK v2 compatible tool creator using the ai library's tool function +// This properly converts Zod schemas to JSON Schema format +export function createTool(config: ToolConfig) { const { name, description, inputSchema, execute } = config; + + // Use the AI SDK's tool function which properly handles Zod to JSON Schema conversion + // Use type assertion to bypass complex type inference issues return tool({ - // name, description, inputSchema, - execute: async (input: any) => { - console.log(`🔧 Tool: ${name}`); - const result = await execute(input); - console.log(`🔧 Tool Result: ${name}`, result); - return result; - // return await logApiExecutionWithTiming(name, input, () => execute(input)); + execute: async (args: z.infer) => { + return await logApiExecutionWithTiming(name, args, () => execute(args)); }, } as any); -}; +} + +// interface ToolConfig { +// name: string; +// description: string; +// inputSchema: z.ZodSchema; +// execute: (args: z.infer) => Promise; +// } + +// export const createTool = (config: ToolConfig) => { +// const { name, description, inputSchema, execute } = config; +// return tool({ +// // name, +// description, +// inputSchema, +// execute: async (input: z.infer) => { +// console.log(`🔧 Tool: ${name}`); +// const result = await execute(input); +// console.log(`🔧 Tool Result: ${name}`, result); +// return result; +// // return await logApiExecutionWithTiming(name, input, () => execute(input)); +// }, +// } as any); +// }; From 9b3cebfdaed4423d3e0b1bcc8aa0a69087bb3529 Mon Sep 17 00:00:00 2001 From: e-roy Date: Tue, 9 Sep 2025 16:33:49 -0400 Subject: [PATCH 12/21] test ai sdk without the logging --- packages/tools/package.json | 2 +- .../tools/src/utils/aisdk-tool-wrapper.ts | 72 +++++++++---------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/tools/package.json b/packages/tools/package.json index e96820d..8082cf9 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.7", + "version": "0.0.11-beta.8", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index 2261e36..abdcb08 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -1,50 +1,50 @@ import { z } from 'zod'; import { tool } from 'ai'; -import { logApiExecutionWithTiming } from './logger'; +// import { logApiExecutionWithTiming } from './logger'; -// Tool configuration interface for AI SDK v2 -export interface ToolConfig { - name: string; - description: string; - inputSchema: T; - execute: (args: z.infer) => Promise; -} - -// AI SDK v2 compatible tool creator using the ai library's tool function -// This properly converts Zod schemas to JSON Schema format -export function createTool(config: ToolConfig) { - const { name, description, inputSchema, execute } = config; - - // Use the AI SDK's tool function which properly handles Zod to JSON Schema conversion - // Use type assertion to bypass complex type inference issues - return tool({ - description, - inputSchema, - execute: async (args: z.infer) => { - return await logApiExecutionWithTiming(name, args, () => execute(args)); - }, - } as any); -} - -// interface ToolConfig { +// // Tool configuration interface for AI SDK v2 +// export interface ToolConfig { // name: string; // description: string; -// inputSchema: z.ZodSchema; +// inputSchema: T; // execute: (args: z.infer) => Promise; // } -// export const createTool = (config: ToolConfig) => { +// // AI SDK v2 compatible tool creator using the ai library's tool function +// // This properly converts Zod schemas to JSON Schema format +// export function createTool(config: ToolConfig) { // const { name, description, inputSchema, execute } = config; + +// // Use the AI SDK's tool function which properly handles Zod to JSON Schema conversion +// // Use type assertion to bypass complex type inference issues // return tool({ -// // name, // description, // inputSchema, -// execute: async (input: z.infer) => { -// console.log(`🔧 Tool: ${name}`); -// const result = await execute(input); -// console.log(`🔧 Tool Result: ${name}`, result); -// return result; -// // return await logApiExecutionWithTiming(name, input, () => execute(input)); +// execute: async (args: z.infer) => { +// return await logApiExecutionWithTiming(name, args, () => execute(args)); // }, // } as any); -// }; +// } + +interface ToolConfig { + name: string; + description: string; + inputSchema: z.ZodSchema; + execute: (input: any) => Promise; +} + +export const createTool = (config: ToolConfig) => { + const { name, description, inputSchema, execute } = config; + return tool({ + // name, + description, + inputSchema, + execute: async (input: any) => { + console.log(`🔧 Tool: ${name}`); + const result = await execute(input); + console.log(`🔧 Tool Result: ${name}`, result); + return result; + // return await logApiExecutionWithTiming(name, input, () => execute(input)); + }, + } as any); +}; From b2599cca13c9bfc4e5d274d29f5293e51f4ff2a4 Mon Sep 17 00:00:00 2001 From: e-roy Date: Tue, 9 Sep 2025 17:35:47 -0400 Subject: [PATCH 13/21] testing with zod v 3 --- packages/tools/README.md | 20 ++++++++++ packages/tools/package.json | 9 +++-- .../tools/src/utils/aisdk-tool-wrapper.ts | 37 ++----------------- pnpm-lock.yaml | 18 ++++----- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/packages/tools/README.md b/packages/tools/README.md index d7af541..1280d44 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -14,6 +14,26 @@ pnpm add fmp-ai-tools yarn add fmp-ai-tools ``` +### Peer Dependencies + +This package requires the following peer dependencies to be installed in your project: + +```bash +# For Vercel AI SDK +npm install ai zod +# or +pnpm add ai zod +# or +yarn add ai zod +``` + +**Required versions:** + +- `ai`: ^5.0.0 +- `zod`: ^3.25.76 || ^4.0.0 + +**⚠️ Common Issue**: If you encounter the error `Invalid schema for function 'getStockQuote': schema must be a JSON Schema of 'type: "object"', got 'type: "None"'`, it means you have a version mismatch between `ai` and `zod`. Make sure you're using compatible versions as listed above. + ## Version Compatibility ### OpenAI Agents Compatibility diff --git a/packages/tools/package.json b/packages/tools/package.json index 8082cf9..f51829d 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.8", + "version": "0.0.11-beta.9", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { @@ -54,10 +54,13 @@ "dependencies": { "@openai/agents": "^0.1.0", "ai": "^5.0.5", - "fmp-node-api": "workspace:*", - "zod": "^3.25.76" + "fmp-node-api": "workspace:*" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.0.0" }, "devDependencies": { + "zod": "^3.25.76", "@types/jest": "^29.5.0", "@types/node": "^20.11.0", "@typescript-eslint/eslint-plugin": "^8.0.0", diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index abdcb08..1e94c49 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -1,30 +1,6 @@ import { z } from 'zod'; -import { tool } from 'ai'; -// import { logApiExecutionWithTiming } from './logger'; - -// // Tool configuration interface for AI SDK v2 -// export interface ToolConfig { -// name: string; -// description: string; -// inputSchema: T; -// execute: (args: z.infer) => Promise; -// } - -// // AI SDK v2 compatible tool creator using the ai library's tool function -// // This properly converts Zod schemas to JSON Schema format -// export function createTool(config: ToolConfig) { -// const { name, description, inputSchema, execute } = config; - -// // Use the AI SDK's tool function which properly handles Zod to JSON Schema conversion -// // Use type assertion to bypass complex type inference issues -// return tool({ -// description, -// inputSchema, -// execute: async (args: z.infer) => { -// return await logApiExecutionWithTiming(name, args, () => execute(args)); -// }, -// } as any); -// } +import { tool, ToolSet } from 'ai'; +import { logApiExecutionWithTiming } from './logger'; interface ToolConfig { name: string; @@ -36,15 +12,10 @@ interface ToolConfig { export const createTool = (config: ToolConfig) => { const { name, description, inputSchema, execute } = config; return tool({ - // name, description, inputSchema, execute: async (input: any) => { - console.log(`🔧 Tool: ${name}`); - const result = await execute(input); - console.log(`🔧 Tool Result: ${name}`, result); - return result; - // return await logApiExecutionWithTiming(name, input, () => execute(input)); + return await logApiExecutionWithTiming(name, input, () => execute(input)); }, - } as any); + } as ToolSet); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00d0874..304fe9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.2 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3) tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -279,9 +279,6 @@ importers: fmp-node-api: specifier: workspace:* version: link:../api - zod: - specifier: ^3.25.76 - version: 3.25.76 devDependencies: '@types/jest': specifier: ^29.5.0 @@ -309,7 +306,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.0 - version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3) tsup: specifier: ^8.0.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) @@ -319,6 +316,9 @@ importers: typescript: specifier: ^5.3.3 version: 5.8.3 + zod: + specifier: ^3.25.76 + version: 3.25.76 packages/types: devDependencies: @@ -10589,12 +10589,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.1) + jest: 29.7.0(@types/node@20.19.2) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -10610,12 +10610,12 @@ snapshots: esbuild: 0.25.5 jest-util: 29.7.0 - ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.2))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.19.2) + jest: 29.7.0(@types/node@20.19.1) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 From 91373e332f8d00e4761cfe3e3e3d40cf3ec3e42b Mon Sep 17 00:00:00 2001 From: e-roy Date: Wed, 10 Sep 2025 08:04:37 -0400 Subject: [PATCH 14/21] changed type for ai sdk fmpTools and cleaned up check version for openai library --- .../src/__tests__/endpoints/calendar.test.ts | 5 -- .../api/src/__tests__/endpoints/list.test.ts | 5 -- packages/tools/package.json | 2 +- .../__tests__/providers/openai/index.test.ts | 1 - .../src/__tests__/utils/version-check.test.ts | 62 +------------------ .../tools/src/providers/vercel-ai/index.ts | 5 +- .../tools/src/utils/aisdk-tool-wrapper.ts | 4 +- packages/tools/src/utils/version-check.ts | 26 ++------ 8 files changed, 13 insertions(+), 97 deletions(-) diff --git a/packages/api/src/__tests__/endpoints/calendar.test.ts b/packages/api/src/__tests__/endpoints/calendar.test.ts index e2344c2..2f6da82 100644 --- a/packages/api/src/__tests__/endpoints/calendar.test.ts +++ b/packages/api/src/__tests__/endpoints/calendar.test.ts @@ -72,9 +72,6 @@ describe('Calendar Endpoints', () => { } fmp = createTestClient(); - // Pre-fetch all test data once to avoid duplicate API calls - console.log('Pre-fetching calendar test data...'); - try { // Use smaller, focused date ranges to reduce API usage const testDateRange = { @@ -100,8 +97,6 @@ describe('Calendar Endpoints', () => { ipo, splits, }; - - console.log('Calendar test data pre-fetched successfully'); } catch (error) { console.warn('Failed to pre-fetch test data:', error); // Continue with tests - they will fetch data individually if needed diff --git a/packages/api/src/__tests__/endpoints/list.test.ts b/packages/api/src/__tests__/endpoints/list.test.ts index 132e3a2..b56c3fc 100644 --- a/packages/api/src/__tests__/endpoints/list.test.ts +++ b/packages/api/src/__tests__/endpoints/list.test.ts @@ -22,9 +22,6 @@ describe('List Endpoints', () => { } fmp = createTestClient(); - // Pre-fetch all list data once to avoid duplicate API calls - console.log('Pre-fetching list test data...'); - try { // Fetch all list data in parallel const [stocks, etfs, crypto, forex, indexes] = await Promise.all([ @@ -42,8 +39,6 @@ describe('List Endpoints', () => { forex, indexes, }; - - console.log('List test data pre-fetched successfully'); } catch (error) { console.warn('Failed to pre-fetch test data:', error); // Continue with tests - they will fetch data individually if needed diff --git a/packages/tools/package.json b/packages/tools/package.json index f51829d..7b93f3a 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.9", + "version": "0.0.11-beta.10", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/tools/src/__tests__/providers/openai/index.test.ts b/packages/tools/src/__tests__/providers/openai/index.test.ts index 681a581..b367f14 100644 --- a/packages/tools/src/__tests__/providers/openai/index.test.ts +++ b/packages/tools/src/__tests__/providers/openai/index.test.ts @@ -3,7 +3,6 @@ import type { Tool } from '@openai/agents'; // Mock the version check to prevent it from running during import jest.mock('@/utils/version-check', () => ({ checkOpenAIAgentsVersion: jest.fn(), - warnOpenAIAgentsVersion: jest.fn(), })); import * as OpenAIProviders from '@/providers/openai'; diff --git a/packages/tools/src/__tests__/utils/version-check.test.ts b/packages/tools/src/__tests__/utils/version-check.test.ts index 921a6f0..e7bb465 100644 --- a/packages/tools/src/__tests__/utils/version-check.test.ts +++ b/packages/tools/src/__tests__/utils/version-check.test.ts @@ -13,36 +13,6 @@ describe('Version Check Utility', () => { console.warn = originalConsoleWarn; }); - describe('warnOpenAIAgentsVersion', () => { - it('should handle errors gracefully', async () => { - const { warnOpenAIAgentsVersion } = await import('../../utils/version-check'); - - // This should not throw, even if there are issues - expect(() => warnOpenAIAgentsVersion()).not.toThrow(); - }); - }); - - describe('error handling', () => { - it('should handle missing package gracefully in warn function', async () => { - const { warnOpenAIAgentsVersion } = await import('../../utils/version-check'); - - // Mock require.resolve to throw an error - const originalResolve = require.resolve; - require.resolve = jest.fn().mockImplementation(() => { - throw new Error('Cannot resolve module'); - }) as unknown as typeof require.resolve; - - warnOpenAIAgentsVersion(); - expect(mockConsoleWarn).toHaveBeenCalledWith( - '⚠️ Version compatibility warning:', - expect.stringContaining('@openai/agents package not found'), - ); - - // Restore original function - require.resolve = originalResolve; - }); - }); - describe('error messages', () => { it('should provide helpful error messages', async () => { const { checkOpenAIAgentsVersion } = await import('../../utils/version-check'); @@ -52,40 +22,10 @@ describe('Version Check Utility', () => { throw new Error('Cannot resolve module'); }) as unknown as typeof require.resolve; - expect(() => checkOpenAIAgentsVersion()).toThrow('npm install @openai/agents@0.1.0'); + expect(() => checkOpenAIAgentsVersion()).toThrow('npm install @openai/agents'); expect(() => checkOpenAIAgentsVersion()).toThrow('@openai/agents package not found'); require.resolve = originalResolve; }); }); - - describe('error scenarios', () => { - it('should handle missing package gracefully', async () => { - const { warnOpenAIAgentsVersion } = await import('../../utils/version-check'); - - // Test that warn function handles errors gracefully - expect(() => warnOpenAIAgentsVersion()).not.toThrow(); - }); - - it('should provide helpful error messages', async () => { - const { checkOpenAIAgentsVersion } = await import('../../utils/version-check'); - - // Test that error messages contain helpful information - try { - checkOpenAIAgentsVersion(); - } catch (error) { - expect((error as Error).message).toContain('@openai/agents package not found'); - expect((error as Error).message).toContain('npm install @openai/agents@0.1.0'); - } - }); - }); - - describe('integration', () => { - it('should work with actual installed version', async () => { - const { warnOpenAIAgentsVersion } = await import('../../utils/version-check'); - - // Test that warn function works with real environment - expect(() => warnOpenAIAgentsVersion()).not.toThrow(); - }); - }); }); diff --git a/packages/tools/src/providers/vercel-ai/index.ts b/packages/tools/src/providers/vercel-ai/index.ts index 2f310bb..f6667f8 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -1,3 +1,4 @@ +import { ToolSet } from 'ai'; import { quoteTools } from './quote'; import { companyTools } from './company'; import { financialTools } from './financial'; @@ -43,7 +44,7 @@ export const { export const { getMarketCap, getStockSplits, getDividendHistory } = stockTools; // Combine all tools into a single object for AI SDK v2 -export const fmpTools = { +export const fmpTools: ToolSet = { ...quoteTools, ...companyTools, ...financialTools, @@ -55,7 +56,7 @@ export const fmpTools = { ...marketTools, ...senateHouseTools, ...stockTools, -} as const; +}; // Re-export individual tool groups export { diff --git a/packages/tools/src/utils/aisdk-tool-wrapper.ts b/packages/tools/src/utils/aisdk-tool-wrapper.ts index 1e94c49..6a9a060 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -2,14 +2,14 @@ import { z } from 'zod'; import { tool, ToolSet } from 'ai'; import { logApiExecutionWithTiming } from './logger'; -interface ToolConfig { +interface AISDKToolConfig { name: string; description: string; inputSchema: z.ZodSchema; execute: (input: any) => Promise; } -export const createTool = (config: ToolConfig) => { +export const createTool = (config: AISDKToolConfig) => { const { name, description, inputSchema, execute } = config; return tool({ description, diff --git a/packages/tools/src/utils/version-check.ts b/packages/tools/src/utils/version-check.ts index 7423bea..f99ec31 100644 --- a/packages/tools/src/utils/version-check.ts +++ b/packages/tools/src/utils/version-check.ts @@ -48,10 +48,10 @@ function compareVersions(v1: string, v2: string): number { } /** - * Checks if a version exactly matches the required version + * Checks if the installed version is less than the required version */ -function matchesExactVersion(installedVersion: string, requiredVersion: string): boolean { - return compareVersions(installedVersion, requiredVersion) === 0; +function isVersionLessThan(installedVersion: string, requiredVersion: string): boolean { + return compareVersions(installedVersion, requiredVersion) < 0; } /** @@ -65,28 +65,14 @@ export function checkOpenAIAgentsVersion(): void { throw new Error( `@openai/agents package not found. ` + `This package requires @openai/agents to be installed. ` + - `Please install with: npm install @openai/agents@${REQUIRED_VERSION}`, + `Please install with: npm install @openai/agents`, ); } - if (!matchesExactVersion(installedVersion, REQUIRED_VERSION)) { + if (isVersionLessThan(installedVersion, REQUIRED_VERSION)) { console.warn( `Incompatible @openai/agents version detected. ` + - `Installed version: ${installedVersion}, Required: ${REQUIRED_VERSION} ` + - `This package requires exactly version ${REQUIRED_VERSION} due to API compatibility. ` + - `Please install with: npm install @openai/agents@${REQUIRED_VERSION}`, + `Installed version: ${installedVersion}, Required: ${REQUIRED_VERSION} or higher.`, ); } } - -/** - * Logs a warning if the version check fails but doesn't throw - */ -export function warnOpenAIAgentsVersion(): void { - try { - checkOpenAIAgentsVersion(); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.warn('⚠️ Version compatibility warning:', errorMessage); - } -} From f6017ff6765ef94547b66f515c153ccba2e7261b Mon Sep 17 00:00:00 2001 From: e-roy Date: Wed, 10 Sep 2025 09:07:21 -0400 Subject: [PATCH 15/21] add discount link and adjust docs --- .env.example | 2 +- README.md | 2 +- .../src/app/docs/api/getting-started/page.mdx | 2 +- .../app/docs/tools/best-practices/page.mdx | 116 ++---------------- apps/docs/src/app/docs/tools/page.mdx | 6 +- .../src/app/docs/tools/vercel-ai/page.mdx | 12 +- apps/docs/src/app/page.tsx | 20 +++ apps/examples/openai/README.md | 2 +- apps/examples/openai/env.example | 2 +- apps/examples/vercel-ai/README.md | 2 +- apps/examples/vercel-ai/env.example | 2 +- 11 files changed, 47 insertions(+), 121 deletions(-) diff --git a/.env.example b/.env.example index 3dce3cd..d8e98d9 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ -# Financial Modeling Prep API Configuration - https://financialmodelingprep.com/developer +# FMP API Key - Get from https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy - 10% off link FMP_API_KEY=your_api_key_here diff --git a/README.md b/README.md index 4341253..0ee2ba4 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ pnpm add fmp-node-api ### 1. Get Your API Key -Sign up at [Financial Modeling Prep](https://site.financialmodelingprep.com/developer/docs/stable) and get your API key. +Sign up at [Financial Modeling Prep](https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy) and get your API key. This link will get you 10% off. ### 2. Basic Usage diff --git a/apps/docs/src/app/docs/api/getting-started/page.mdx b/apps/docs/src/app/docs/api/getting-started/page.mdx index 947cd73..d02af79 100644 --- a/apps/docs/src/app/docs/api/getting-started/page.mdx +++ b/apps/docs/src/app/docs/api/getting-started/page.mdx @@ -130,7 +130,7 @@ const fmp = new FMP(); // Automatically uses FMP_API_KEY from environment Create a `.env` file in your project root: -{`# Your FMP API key (get one at https://financialmodelingprep.com/developer) +{`# Your FMP API key (get one at https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy) FMP_API_KEY=your-api-key-here`} Or set it in your system environment: diff --git a/apps/docs/src/app/docs/tools/best-practices/page.mdx b/apps/docs/src/app/docs/tools/best-practices/page.mdx index cc72db0..6983ded 100644 --- a/apps/docs/src/app/docs/tools/best-practices/page.mdx +++ b/apps/docs/src/app/docs/tools/best-practices/page.mdx @@ -53,7 +53,7 @@ Use `maxSteps` or `stopWhen` to prevent excessive API calls: {`import { openai } from '@ai-sdk/openai'; -import { streamText, convertToModelMessages, stepCountIs } from 'ai'; +import { streamText, convertToModelMessages, stepCountIs, ToolSet } from 'ai'; import { fmpTools } from 'fmp-ai-tools/vercel-ai'; export async function POST(req: Request) { @@ -62,7 +62,7 @@ export async function POST(req: Request) { const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), -tools: fmpTools, +tools: fmpTools as ToolSet, stopWhen: stepCountIs(5), // Limit to 5 tool calls }); @@ -188,7 +188,7 @@ Use appropriate model settings: maxTokens: 1000, // Limit response length }), messages: convertToModelMessages(messages), - tools: fmpTools, + tools: fmpTools as ToolSet, stopWhen: stepCountIs(3), // Limit tool usage });`} @@ -228,17 +228,14 @@ OPENAI_API_KEY=your_openai_key_here`} Track which tools are being used: - - {`const result = streamText({ - model: openai('gpt-4o-mini'), - messages: convertToModelMessages(messages), - tools: fmpTools, - experimental_onToolCall: ({ toolCall }) => { - console.log('Tool used:', toolCall.toolName); - // Log to your analytics service - analytics.track('tool_used', { tool: toolCall.toolName }); - }, -});`} + + {`# To enable detailed logging of API calls and responses - default is false +FMP_TOOLS_LOG_API_RESULTS=true + +# To enable logging of data from API calls - default is false + +FMP_TOOLS_LOG_DATA_ONLY=true`} + ### Monitor Error Rates @@ -256,97 +253,6 @@ Track errors to identify issues: });`} -## Production Considerations - -### Environment Configuration - -Use different settings for development and production: - - -{`const isProduction = process.env.NODE_ENV === 'production'; - -const result = streamText({ -model: openai('gpt-4o-mini'), -messages: convertToModelMessages(messages), -tools: fmpTools, -stopWhen: isProduction ? stepCountIs(3) : stepCountIs(5), // Stricter limits in production -temperature: isProduction ? 0.5 : 0.7, // More conservative in production -});`} - - - -### Health Checks - -Implement health checks for your API routes: - - - {`export async function GET() { - try { - // Test basic functionality - const testResult = await fetch('https://api.example.com/health'); - - return new Response(JSON.stringify({ - status: 'healthy', - timestamp: new Date().toISOString() - }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); - } catch (error) { - return new Response(JSON.stringify({ - status: 'unhealthy', - error: error.message - }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } -}`} - - -## Testing - -### Unit Tests - -Test your tool integration: - - -{`import { describe, it, expect } from 'vitest'; -import { fmpTools } from 'fmp-ai-tools/vercel-ai'; - -describe('FMP Tools', () => { -it('should have required tools', () => { -expect(fmpTools.getStockQuote).toBeDefined(); -expect(fmpTools.getCompanyProfile).toBeDefined(); -}); -});`} - - - -### Integration Tests - -Test the full chat flow: - - -{`import { describe, it, expect } from 'vitest'; - -describe('Chat API', () => { -it('should handle stock quote requests', async () => { -const response = await fetch('/api/chat', { -method: 'POST', -headers: { 'Content-Type': 'application/json' }, -body: JSON.stringify({ -messages: [{ role: 'user', content: 'What is the price of AAPL?' }] -}) -}); - - expect(response.status).toBe(200); - -}); -});`} - - - ## Summary 1. **Choose the right tools** for your use case diff --git a/apps/docs/src/app/docs/tools/page.mdx b/apps/docs/src/app/docs/tools/page.mdx index 9a37867..6a570a5 100644 --- a/apps/docs/src/app/docs/tools/page.mdx +++ b/apps/docs/src/app/docs/tools/page.mdx @@ -20,7 +20,7 @@ FMP Tools provides pre-built AI tools that can be used with various AI framework {`import { openai } from '@ai-sdk/openai'; -import { streamText, convertToModelMessages, stepCountIs } from 'ai'; +import { streamText, convertToModelMessages, stepCountIs, ToolSet } from 'ai'; import { fmpTools } from 'fmp-ai-tools/vercel-ai'; export async function POST(req: Request) { @@ -29,7 +29,7 @@ export async function POST(req: Request) { const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), -tools: fmpTools, +tools: fmpTools as ToolSet, stopWhen: stepCountIs(5), }); @@ -151,7 +151,7 @@ const selectedTools = { const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), -tools: selectedTools, +tools: selectedTools as ToolSet, });`} diff --git a/apps/docs/src/app/docs/tools/vercel-ai/page.mdx b/apps/docs/src/app/docs/tools/vercel-ai/page.mdx index ae2b351..a05bc71 100644 --- a/apps/docs/src/app/docs/tools/vercel-ai/page.mdx +++ b/apps/docs/src/app/docs/tools/vercel-ai/page.mdx @@ -36,7 +36,7 @@ FMP_TOOLS_LOG_DATA_ONLY=false`} {`import { openai } from '@ai-sdk/openai'; -import { streamText, convertToModelMessages, stepCountIs } from 'ai'; +import { streamText, convertToModelMessages, stepCountIs, ToolSet } from 'ai'; import { fmpTools } from 'fmp-ai-tools/vercel-ai'; export async function POST(req: Request) { @@ -45,7 +45,7 @@ export async function POST(req: Request) { const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), -tools: fmpTools, +tools: fmpTools as ToolSet, stopWhen: stepCountIs(5), }); @@ -135,7 +135,7 @@ You can select specific tools instead of using all available tools: {`import { openai } from '@ai-sdk/openai'; -import { streamText, convertToModelMessages } from 'ai'; +import { streamText, convertToModelMessages, ToolSet } from 'ai'; import { quoteTools, financialTools } from 'fmp-ai-tools/vercel-ai'; // Use only quote and financial tools @@ -150,7 +150,7 @@ export async function POST(req: Request) { const result = streamText({ model: openai('gpt-4o-mini'), messages: convertToModelMessages(messages), -tools: selectedTools, +tools: selectedTools as ToolSet, }); return result.toUIMessageStreamResponse(); @@ -162,7 +162,7 @@ return result.toUIMessageStreamResponse(); {`import { openai } from '@ai-sdk/openai'; -import { streamText, convertToModelMessages } from 'ai'; +import { streamText, convertToModelMessages, ToolSet } from 'ai'; import { fmpTools } from 'fmp-ai-tools/vercel-ai'; export async function POST(req: Request) { @@ -174,7 +174,7 @@ temperature: 0.7, maxTokens: 1000, }), messages: convertToModelMessages(messages), -tools: fmpTools, +tools: fmpTools as ToolSet, stopWhen: stepCountIs(3), // Limit tool usage system: \`You are a helpful financial assistant. Use the available tools to provide accurate financial information. Always cite your sources when providing data.\`, }); diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx index 2c1ab9c..094d99a 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -24,6 +24,26 @@ export default function Home() {

A comprehensive Node.js ecosystem for the Financial Modeling Prep API

+ + + + Financial Modeling Prep API Key + + + Link for 10% off + + + + + https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy + + + {/* Main Library Selection */} diff --git a/apps/examples/openai/README.md b/apps/examples/openai/README.md index 14bcc05..25711d1 100644 --- a/apps/examples/openai/README.md +++ b/apps/examples/openai/README.md @@ -21,7 +21,7 @@ Additional tools from the FMP Tools library can be easily added to expand functi ### Prerequisites -1. **FMP API Key** - Get your API key from [Financial Modeling Prep](https://financialmodelingprep.com/developer/docs/) +1. **FMP API Key** - Get your API key from [Financial Modeling Prep](https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy) - Link for 10% off 2. **OpenAI API Key** - Get your API key from [OpenAI](https://platform.openai.com/api-keys) ### Environment Variables diff --git a/apps/examples/openai/env.example b/apps/examples/openai/env.example index b64e69c..ec6f1d1 100644 --- a/apps/examples/openai/env.example +++ b/apps/examples/openai/env.example @@ -1,4 +1,4 @@ -# FMP API Key - Get from https://financialmodelingprep.com/developer/docs/ +# FMP API Key - Get from https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy - 10% off link FMP_API_KEY=your_fmp_api_key_here # OpenAI API Key - Get from https://platform.openai.com/api-keys diff --git a/apps/examples/vercel-ai/README.md b/apps/examples/vercel-ai/README.md index 921e0cf..aa323c4 100644 --- a/apps/examples/vercel-ai/README.md +++ b/apps/examples/vercel-ai/README.md @@ -26,7 +26,7 @@ The example includes access to all FMP tools: ### Prerequisites -1. **FMP API Key** - Get your API key from [Financial Modeling Prep](https://financialmodelingprep.com/developer/docs/) +1. **FMP API Key** - Get your API key from [Financial Modeling Prep](https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy) - Link for 10% off 2. **OpenAI API Key** - Get your API key from [OpenAI](https://platform.openai.com/api-keys) **Note**: This example uses the Vercel AI SDK v5.0.5 with AI SDK providers v2.0.3. diff --git a/apps/examples/vercel-ai/env.example b/apps/examples/vercel-ai/env.example index b64e69c..ec6f1d1 100644 --- a/apps/examples/vercel-ai/env.example +++ b/apps/examples/vercel-ai/env.example @@ -1,4 +1,4 @@ -# FMP API Key - Get from https://financialmodelingprep.com/developer/docs/ +# FMP API Key - Get from https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy - 10% off link FMP_API_KEY=your_fmp_api_key_here # OpenAI API Key - Get from https://platform.openai.com/api-keys From 53b9a63f0f45048f4e844704a9aa9b5f9b88071c Mon Sep 17 00:00:00 2001 From: e-roy Date: Wed, 10 Sep 2025 16:09:33 -0400 Subject: [PATCH 16/21] expand financial tools --- packages/api/scripts/test-endpoint.ts | 3 +- .../providers/openai/financial.test.ts | 94 ++++++++ .../__tests__/providers/openai/index.test.ts | 7 + .../providers/vercel-ai/index.test.ts | 15 ++ .../tools/src/providers/openai/financial.ts | 225 ++++++++++++++++-- packages/tools/src/providers/openai/index.ts | 28 +++ .../src/providers/vercel-ai/financial.ts | 153 +++++++++++- .../tools/src/providers/vercel-ai/index.ts | 15 +- 8 files changed, 505 insertions(+), 35 deletions(-) diff --git a/packages/api/scripts/test-endpoint.ts b/packages/api/scripts/test-endpoint.ts index a307668..409a11e 100644 --- a/packages/api/scripts/test-endpoint.ts +++ b/packages/api/scripts/test-endpoint.ts @@ -204,7 +204,7 @@ async function testEndpoint() { case 'key-metrics': result = await fmp.financial.getKeyMetrics({ symbol: 'AAPL', - period: 'annual', + period: 'quarter', limit: 2, }); break; @@ -550,6 +550,7 @@ async function testEndpoint() { } } else { console.log('❌ Failed:'); + console.log(result); console.log(result.error); } } catch (error) { diff --git a/packages/tools/src/__tests__/providers/openai/financial.test.ts b/packages/tools/src/__tests__/providers/openai/financial.test.ts index 5416e6b..78e9d6c 100644 --- a/packages/tools/src/__tests__/providers/openai/financial.test.ts +++ b/packages/tools/src/__tests__/providers/openai/financial.test.ts @@ -2,14 +2,28 @@ import { getBalanceSheet, getIncomeStatement, getCashFlowStatement, + getKeyMetrics, getFinancialRatios, + getEnterpriseValue, + getCashflowGrowth, + getIncomeGrowth, + getBalanceSheetGrowth, + getFinancialGrowth, + getEarningsHistorical, } from '@/providers/openai/financial'; const mockFinancial = { getBalanceSheet: jest.fn(), getIncomeStatement: jest.fn(), getCashFlowStatement: jest.fn(), + getKeyMetrics: jest.fn(), getFinancialRatios: jest.fn(), + getEnterpriseValue: jest.fn(), + getCashflowGrowth: jest.fn(), + getIncomeGrowth: jest.fn(), + getBalanceSheetGrowth: jest.fn(), + getFinancialGrowth: jest.fn(), + getEarningsHistorical: jest.fn(), }; jest.mock('@/client', () => ({ @@ -29,6 +43,7 @@ describe('OpenAI Financial Tools (minimal)', () => { expect(mockFinancial.getBalanceSheet).toHaveBeenCalledWith({ symbol: 'AAPL', period: 'annual', + limit: 5, }); expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); }); @@ -39,6 +54,7 @@ describe('OpenAI Financial Tools (minimal)', () => { expect(mockFinancial.getIncomeStatement).toHaveBeenCalledWith({ symbol: 'AAPL', period: 'annual', + limit: 5, }); expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); }); @@ -49,6 +65,18 @@ describe('OpenAI Financial Tools (minimal)', () => { expect(mockFinancial.getCashFlowStatement).toHaveBeenCalledWith({ symbol: 'AAPL', period: 'annual', + limit: 5, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); + + it('getKeyMetrics executes and returns data', async () => { + mockFinancial.getKeyMetrics.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + const result = await (getKeyMetrics as any).execute({ symbol: 'AAPL' }); + expect(mockFinancial.getKeyMetrics).toHaveBeenCalledWith({ + symbol: 'AAPL', + period: 'annual', + limit: 5, }); expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); }); @@ -59,6 +87,72 @@ describe('OpenAI Financial Tools (minimal)', () => { expect(mockFinancial.getFinancialRatios).toHaveBeenCalledWith({ symbol: 'AAPL', period: 'annual', + limit: 5, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); + + it('getEnterpriseValue executes and returns data', async () => { + mockFinancial.getEnterpriseValue.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + const result = await (getEnterpriseValue as any).execute({ symbol: 'AAPL' }); + expect(mockFinancial.getEnterpriseValue).toHaveBeenCalledWith({ + symbol: 'AAPL', + period: 'annual', + limit: 5, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); + + it('getCashflowGrowth executes and returns data', async () => { + mockFinancial.getCashflowGrowth.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + const result = await (getCashflowGrowth as any).execute({ symbol: 'AAPL' }); + expect(mockFinancial.getCashflowGrowth).toHaveBeenCalledWith({ + symbol: 'AAPL', + period: 'annual', + limit: 5, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); + + it('getIncomeGrowth executes and returns data', async () => { + mockFinancial.getIncomeGrowth.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + const result = await (getIncomeGrowth as any).execute({ symbol: 'AAPL' }); + expect(mockFinancial.getIncomeGrowth).toHaveBeenCalledWith({ + symbol: 'AAPL', + period: 'annual', + limit: 5, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); + + it('getBalanceSheetGrowth executes and returns data', async () => { + mockFinancial.getBalanceSheetGrowth.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + const result = await (getBalanceSheetGrowth as any).execute({ symbol: 'AAPL' }); + expect(mockFinancial.getBalanceSheetGrowth).toHaveBeenCalledWith({ + symbol: 'AAPL', + period: 'annual', + limit: 5, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); + + it('getFinancialGrowth executes and returns data', async () => { + mockFinancial.getFinancialGrowth.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + const result = await (getFinancialGrowth as any).execute({ symbol: 'AAPL' }); + expect(mockFinancial.getFinancialGrowth).toHaveBeenCalledWith({ + symbol: 'AAPL', + period: 'annual', + limit: 5, + }); + expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); + }); + + it('getEarningsHistorical executes and returns data', async () => { + mockFinancial.getEarningsHistorical.mockResolvedValueOnce({ data: [{ symbol: 'AAPL' }] }); + const result = await (getEarningsHistorical as any).execute({ symbol: 'AAPL' }); + expect(mockFinancial.getEarningsHistorical).toHaveBeenCalledWith({ + symbol: 'AAPL', + limit: 10, }); expect(JSON.parse(result)).toEqual([{ symbol: 'AAPL' }]); }); diff --git a/packages/tools/src/__tests__/providers/openai/index.test.ts b/packages/tools/src/__tests__/providers/openai/index.test.ts index b367f14..17449ac 100644 --- a/packages/tools/src/__tests__/providers/openai/index.test.ts +++ b/packages/tools/src/__tests__/providers/openai/index.test.ts @@ -20,7 +20,14 @@ describe('OpenAI providers index exports', () => { 'getBalanceSheet', 'getIncomeStatement', 'getCashFlowStatement', + 'getKeyMetrics', 'getFinancialRatios', + 'getEnterpriseValue', + 'getCashflowGrowth', + 'getIncomeGrowth', + 'getBalanceSheetGrowth', + 'getFinancialGrowth', + 'getEarningsHistorical', 'getInsiderTrading', 'getInstitutionalHolders', 'getMarketPerformance', diff --git a/packages/tools/src/__tests__/providers/vercel-ai/index.test.ts b/packages/tools/src/__tests__/providers/vercel-ai/index.test.ts index 87556f2..ad6aaac 100644 --- a/packages/tools/src/__tests__/providers/vercel-ai/index.test.ts +++ b/packages/tools/src/__tests__/providers/vercel-ai/index.test.ts @@ -7,7 +7,15 @@ const mockClient = { getBalanceSheet: jest.fn().mockResolvedValue({ data: [] }), getIncomeStatement: jest.fn().mockResolvedValue({ data: [] }), getCashFlowStatement: jest.fn().mockResolvedValue({ data: [] }), + getKeyMetrics: jest.fn().mockResolvedValue({ data: [] }), getFinancialRatios: jest.fn().mockResolvedValue({ data: [] }), + getEnterpriseValue: jest.fn().mockResolvedValue({ data: [] }), + getCashflowGrowth: jest.fn().mockResolvedValue({ data: [] }), + getIncomeGrowth: jest.fn().mockResolvedValue({ data: [] }), + getBalanceSheetGrowth: jest.fn().mockResolvedValue({ data: [] }), + getFinancialGrowth: jest.fn().mockResolvedValue({ data: [] }), + getEarningsHistorical: jest.fn().mockResolvedValue({ data: [] }), + getEarningsSurprises: jest.fn().mockResolvedValue({ data: [] }), }, calendar: { getEarningsCalendar: jest.fn().mockResolvedValue({ data: [] }), @@ -85,7 +93,14 @@ describe('Vercel AI Provider Index (minimal)', () => { getBalanceSheet: { symbol: 'AAPL', period: 'annual' }, getIncomeStatement: { symbol: 'AAPL', period: 'annual' }, getCashFlowStatement: { symbol: 'AAPL', period: 'annual' }, + getKeyMetrics: { symbol: 'AAPL', period: 'annual' }, getFinancialRatios: { symbol: 'AAPL', period: 'annual' }, + getEnterpriseValue: { symbol: 'AAPL', period: 'annual' }, + getCashflowGrowth: { symbol: 'AAPL', period: 'annual' }, + getIncomeGrowth: { symbol: 'AAPL', period: 'annual' }, + getBalanceSheetGrowth: { symbol: 'AAPL', period: 'annual' }, + getFinancialGrowth: { symbol: 'AAPL', period: 'annual' }, + getEarningsHistorical: { symbol: 'AAPL' }, getEarningsCalendar: { from: '2024-01-01', to: '2024-01-31' }, getEconomicCalendar: { from: '2024-01-01', to: '2024-01-31' }, getTreasuryRates: { from: '2024-01-01', to: '2024-01-31' }, diff --git a/packages/tools/src/providers/openai/financial.ts b/packages/tools/src/providers/openai/financial.ts index 8fa6494..b4973aa 100644 --- a/packages/tools/src/providers/openai/financial.ts +++ b/packages/tools/src/providers/openai/financial.ts @@ -2,25 +2,24 @@ import { z } from 'zod'; import { createOpenAITool } from '@/utils/openai-tool-wrapper'; import { getFMPClient } from '@/client'; -// Common input schema for financial statements with symbol and period -const financialStatementInputSchema = z.object({ - symbol: z - .string() - .min(1, 'Stock symbol is required') - .describe('The stock symbol (e.g., AAPL, MSFT, GOOGL)'), - period: z - .enum(['annual', 'quarter']) - .default('annual') - .describe('The period type (annual or quarter)'), -}); - export const getBalanceSheet = createOpenAITool({ name: 'getBalanceSheet', description: 'Get balance sheet for a company showing assets, liabilities, and equity', - inputSchema: financialStatementInputSchema, - execute: async ({ symbol, period }) => { + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get balance sheet for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); - const balanceSheet = await fmp.financial.getBalanceSheet({ symbol, period }); + const balanceSheet = await fmp.financial.getBalanceSheet({ + symbol, + period, + limit: Number(limit), + }); return JSON.stringify(balanceSheet.data, null, 2); }, }); @@ -28,10 +27,21 @@ export const getBalanceSheet = createOpenAITool({ export const getIncomeStatement = createOpenAITool({ name: 'getIncomeStatement', description: 'Get income statement for a company showing revenue, expenses, and profit', - inputSchema: financialStatementInputSchema, - execute: async ({ symbol, period }) => { + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get income statement for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); - const incomeStatement = await fmp.financial.getIncomeStatement({ symbol, period }); + const incomeStatement = await fmp.financial.getIncomeStatement({ + symbol, + period, + limit: Number(limit), + }); return JSON.stringify(incomeStatement.data, null, 2); }, }); @@ -40,22 +50,189 @@ export const getCashFlowStatement = createOpenAITool({ name: 'getCashFlowStatement', description: 'Get cash flow statement for a company showing operating, investing, and financing cash flows', - inputSchema: financialStatementInputSchema, - execute: async ({ symbol, period }) => { + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get cash flow statement for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); - const cashFlowStatement = await fmp.financial.getCashFlowStatement({ symbol, period }); + const cashFlowStatement = await fmp.financial.getCashFlowStatement({ + symbol, + period, + limit: Number(limit), + }); return JSON.stringify(cashFlowStatement.data, null, 2); }, }); +export const getKeyMetrics = createOpenAITool({ + name: 'getKeyMetrics', + description: 'Get key metrics for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get key metrics for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const keyMetrics = await fmp.financial.getKeyMetrics({ symbol, period, limit: Number(limit) }); + return JSON.stringify(keyMetrics.data, null, 2); + }, +}); + export const getFinancialRatios = createOpenAITool({ name: 'getFinancialRatios', description: 'Get financial ratios for a company including profitability, liquidity, and efficiency metrics', - inputSchema: financialStatementInputSchema, - execute: async ({ symbol, period }) => { + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get financial ratios for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); - const financialRatios = await fmp.financial.getFinancialRatios({ symbol, period }); + const financialRatios = await fmp.financial.getFinancialRatios({ + symbol, + period, + limit: Number(limit), + }); return JSON.stringify(financialRatios.data, null, 2); }, }); + +export const getEnterpriseValue = createOpenAITool({ + name: 'getEnterpriseValue', + description: 'Get enterprise value for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get enterprise value for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const enterpriseValue = await fmp.financial.getEnterpriseValue({ + symbol, + period, + limit: Number(limit), + }); + return JSON.stringify(enterpriseValue.data, null, 2); + }, +}); + +export const getCashflowGrowth = createOpenAITool({ + name: 'getCashflowGrowth', + description: 'Get cashflow growth for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get cashflow growth for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const cashflowGrowth = await fmp.financial.getCashflowGrowth({ + symbol, + period, + limit: Number(limit), + }); + return JSON.stringify(cashflowGrowth.data, null, 2); + }, +}); + +export const getIncomeGrowth = createOpenAITool({ + name: 'getIncomeGrowth', + description: 'Get income growth for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get income growth for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const incomeGrowth = await fmp.financial.getIncomeGrowth({ + symbol, + period, + limit: Number(limit), + }); + return JSON.stringify(incomeGrowth.data, null, 2); + }, +}); + +export const getBalanceSheetGrowth = createOpenAITool({ + name: 'getBalanceSheetGrowth', + description: 'Get balance sheet growth for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get balance sheet growth for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const balanceSheetGrowth = await fmp.financial.getBalanceSheetGrowth({ + symbol, + period, + limit: Number(limit), + }); + return JSON.stringify(balanceSheetGrowth.data, null, 2); + }, +}); + +export const getFinancialGrowth = createOpenAITool({ + name: 'getFinancialGrowth', + description: 'Get financial growth for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get financial growth for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.string().default('5').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const financialGrowth = await fmp.financial.getFinancialGrowth({ + symbol, + period, + limit: Number(limit), + }); + return JSON.stringify(financialGrowth.data, null, 2); + }, +}); + +export const getEarningsHistorical = createOpenAITool({ + name: 'getEarningsHistorical', + description: 'Get earnings historical for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get earnings historical for'), + limit: z.string().default('10').describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, limit }) => { + const fmp = getFMPClient(); + const earningsHistorical = await fmp.financial.getEarningsHistorical({ + symbol, + limit: Number(limit), + }); + return JSON.stringify(earningsHistorical.data, null, 2); + }, +}); diff --git a/packages/tools/src/providers/openai/index.ts b/packages/tools/src/providers/openai/index.ts index b9bd292..06c4066 100644 --- a/packages/tools/src/providers/openai/index.ts +++ b/packages/tools/src/providers/openai/index.ts @@ -8,7 +8,14 @@ import { getBalanceSheet, getIncomeStatement, getCashFlowStatement, + getKeyMetrics, getFinancialRatios, + getEnterpriseValue, + getCashflowGrowth, + getIncomeGrowth, + getBalanceSheetGrowth, + getFinancialGrowth, + getEarningsHistorical, } from './financial'; import { getInsiderTrading } from './insider'; import { getInstitutionalHolders } from './institutional'; @@ -42,7 +49,14 @@ export { getBalanceSheet, getIncomeStatement, getCashFlowStatement, + getKeyMetrics, getFinancialRatios, + getEnterpriseValue, + getCashflowGrowth, + getIncomeGrowth, + getBalanceSheetGrowth, + getFinancialGrowth, + getEarningsHistorical, getInsiderTrading, getInstitutionalHolders, getMarketPerformance, @@ -71,7 +85,14 @@ export const financialTools = [ getBalanceSheet, getIncomeStatement, getCashFlowStatement, + getKeyMetrics, getFinancialRatios, + getEnterpriseValue, + getCashflowGrowth, + getIncomeGrowth, + getBalanceSheetGrowth, + getFinancialGrowth, + getEarningsHistorical, ] as Tool[]; export const insiderTools = [getInsiderTrading] as Tool[]; export const institutionalTools = [getInstitutionalHolders] as Tool[]; @@ -105,7 +126,14 @@ export const fmpTools: Tool[] = [ getBalanceSheet, getIncomeStatement, getCashFlowStatement, + getKeyMetrics, getFinancialRatios, + getEnterpriseValue, + getCashflowGrowth, + getIncomeGrowth, + getBalanceSheetGrowth, + getFinancialGrowth, + getEarningsHistorical, getInsiderTrading, getInstitutionalHolders, getMarketPerformance, diff --git a/packages/tools/src/providers/vercel-ai/financial.ts b/packages/tools/src/providers/vercel-ai/financial.ts index 778a019..a2e3617 100644 --- a/packages/tools/src/providers/vercel-ai/financial.ts +++ b/packages/tools/src/providers/vercel-ai/financial.ts @@ -12,10 +12,11 @@ export const financialTools = { .enum(['annual', 'quarter']) .default('annual') .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), }), - execute: async ({ symbol, period }) => { + execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); - const balanceSheet = await fmp.financial.getBalanceSheet({ symbol, period }); + const balanceSheet = await fmp.financial.getBalanceSheet({ symbol, period, limit }); const response = JSON.stringify(balanceSheet.data, null, 2); return response; }, @@ -30,10 +31,11 @@ export const financialTools = { .enum(['annual', 'quarter']) .default('annual') .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), }), - execute: async ({ symbol, period }) => { + execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); - const incomeStatement = await fmp.financial.getIncomeStatement({ symbol, period }); + const incomeStatement = await fmp.financial.getIncomeStatement({ symbol, period, limit }); const response = JSON.stringify(incomeStatement.data, null, 2); return response; }, @@ -48,15 +50,35 @@ export const financialTools = { .enum(['annual', 'quarter']) .default('annual') .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), }), - execute: async ({ symbol, period }) => { + execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); - const cashFlowStatement = await fmp.financial.getCashFlowStatement({ symbol, period }); + const cashFlowStatement = await fmp.financial.getCashFlowStatement({ symbol, period, limit }); const response = JSON.stringify(cashFlowStatement.data, null, 2); return response; }, }), + getKeyMetrics: createTool({ + name: 'getKeyMetrics', + description: 'Get key metrics for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get key metrics for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const keyMetrics = await fmp.financial.getKeyMetrics({ symbol, period, limit }); + const response = JSON.stringify(keyMetrics.data, null, 2); + return response; + }, + }), + getFinancialRatios: createTool({ name: 'getFinancialRatios', description: 'Get financial ratios for a company', @@ -66,12 +88,127 @@ export const financialTools = { .enum(['annual', 'quarter']) .default('annual') .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), }), - execute: async ({ symbol, period }) => { + execute: async ({ symbol, period, limit }) => { const fmp = getFMPClient(); - const financialRatios = await fmp.financial.getFinancialRatios({ symbol, period }); + const financialRatios = await fmp.financial.getFinancialRatios({ symbol, period, limit }); const response = JSON.stringify(financialRatios.data, null, 2); return response; }, }), + + getEnterpriseValue: createTool({ + name: 'getEnterpriseValue', + description: 'Get enterprise value for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get enterprise value for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const enterpriseValue = await fmp.financial.getEnterpriseValue({ symbol, period, limit }); + const response = JSON.stringify(enterpriseValue.data, null, 2); + return response; + }, + }), + + getCashflowGrowth: createTool({ + name: 'getCashflowGrowth', + description: 'Get cashflow growth for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get cashflow growth for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const cashflowGrowth = await fmp.financial.getCashflowGrowth({ symbol, period, limit }); + const response = JSON.stringify(cashflowGrowth.data, null, 2); + return response; + }, + }), + + getIncomeGrowth: createTool({ + name: 'getIncomeGrowth', + description: 'Get income growth for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get income growth for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const incomeGrowth = await fmp.financial.getIncomeGrowth({ symbol, period, limit }); + const response = JSON.stringify(incomeGrowth.data, null, 2); + return response; + }, + }), + + getBalanceSheetGrowth: createTool({ + name: 'getBalanceSheetGrowth', + description: 'Get balance sheet growth for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get balance sheet growth for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const balanceSheetGrowth = await fmp.financial.getBalanceSheetGrowth({ + symbol, + period, + limit, + }); + const response = JSON.stringify(balanceSheetGrowth.data, null, 2); + return response; + }, + }), + + getFinancialGrowth: createTool({ + name: 'getFinancialGrowth', + description: 'Get financial growth for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get financial growth for'), + period: z + .enum(['annual', 'quarter']) + .default('annual') + .describe('The period type (annual or quarter)'), + limit: z.number().default(5).describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, period, limit }) => { + const fmp = getFMPClient(); + const financialGrowth = await fmp.financial.getFinancialGrowth({ symbol, period, limit }); + const response = JSON.stringify(financialGrowth.data, null, 2); + return response; + }, + }), + + getEarningsHistorical: createTool({ + name: 'getEarningsHistorical', + description: 'Get earnings historical for a company', + inputSchema: z.object({ + symbol: z.string().describe('The stock symbol to get earnings historical for'), + limit: z.number().default(10).describe('The number of periods to retrieve'), + }), + execute: async ({ symbol, limit }) => { + const fmp = getFMPClient(); + const earningsHistorical = await fmp.financial.getEarningsHistorical({ symbol, limit }); + const response = JSON.stringify(earningsHistorical.data, null, 2); + return response; + }, + }), }; diff --git a/packages/tools/src/providers/vercel-ai/index.ts b/packages/tools/src/providers/vercel-ai/index.ts index f6667f8..26401f9 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -20,8 +20,19 @@ export const { getTreasuryRates, getEconomicIndicators } = economicTools; export const { getETFHoldings, getETFProfile } = etfTools; -export const { getBalanceSheet, getIncomeStatement, getCashFlowStatement, getFinancialRatios } = - financialTools; +export const { + getBalanceSheet, + getIncomeStatement, + getCashFlowStatement, + getKeyMetrics, + getFinancialRatios, + getEnterpriseValue, + getCashflowGrowth, + getIncomeGrowth, + getBalanceSheetGrowth, + getFinancialGrowth, + getEarningsHistorical, +} = financialTools; export const { getInsiderTrading } = insiderTools; From 67cab477ce52b43f069eac66a727f3a4cd9d2aa6 Mon Sep 17 00:00:00 2001 From: e-roy Date: Wed, 10 Sep 2025 17:07:02 -0400 Subject: [PATCH 17/21] update some market api calls to stable --- apps/docs/src/app/docs/api/market/page.mdx | 6 +++--- packages/api/src/endpoints/market.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/docs/src/app/docs/api/market/page.mdx b/apps/docs/src/app/docs/api/market/page.mdx index 098cfc1..c4e4c2e 100644 --- a/apps/docs/src/app/docs/api/market/page.mdx +++ b/apps/docs/src/app/docs/api/market/page.mdx @@ -18,17 +18,17 @@ Access market-wide data including performance metrics, trading hours, sector inf }, { method: 'GET', - path: '/stock_market/gainers', + path: '/stable/biggest-gainers', description: 'Get top gaining stocks', }, { method: 'GET', - path: '/stock_market/losers', + path: '/stable/biggest-losers', description: 'Get top losing stocks', }, { method: 'GET', - path: '/stock_market/actives', + path: '/stable/most-actives', description: 'Get most active stocks', }, { diff --git a/packages/api/src/endpoints/market.ts b/packages/api/src/endpoints/market.ts index ee878d7..c620fbd 100644 --- a/packages/api/src/endpoints/market.ts +++ b/packages/api/src/endpoints/market.ts @@ -109,10 +109,10 @@ export class MarketEndpoints { * console.log(`High volume gainers (>1M shares): ${highVolumeGainers.length}`); * ``` * - * @see {@link https://site.financialmodelingprep.com/developer/docs#market-biggest-gainers-market-overview|FMP Market Biggest Gainers Documentation} + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#biggest-gainers|FMP Market Biggest Gainers Documentation} */ async getGainers(): Promise> { - return this.client.get('/stock_market/gainers', 'v3'); + return this.client.get('/biggest-gainers', 'stable'); } /** @@ -151,10 +151,10 @@ export class MarketEndpoints { * ); * ``` * - * @see {@link https://site.financialmodelingprep.com/developer/docs#market-biggest-losers-market-overview|FMP Market Biggest Losers Documentation} + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#biggest-losers|FMP Market Biggest Losers Documentation} */ async getLosers(): Promise> { - return this.client.get('/stock_market/losers', 'v3'); + return this.client.get('/biggest-losers', 'stable'); } /** @@ -198,10 +198,10 @@ export class MarketEndpoints { * ); * ``` * - * @see {@link https://site.financialmodelingprep.com/developer/docs#market-most-actives|FMP Market Most Actives Documentation} + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#most-active|FMP Market Most Actives Documentation} */ async getMostActive(): Promise> { - return this.client.get('/stock_market/actives', 'v3'); + return this.client.get('/most-actives', 'stable'); } /** From c5761d928730ad42027ba8018edc032cf13c2bac Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 11 Sep 2025 10:00:59 -0400 Subject: [PATCH 18/21] feat: update senate house to stable api --- .../src/app/docs/api/senate-house/page.mdx | 157 ++++++++++-------- .../__tests__/endpoints/senate-house.test.ts | 60 ++----- packages/api/src/endpoints/senate-house.ts | 22 +-- packages/types/src/senate-house.ts | 23 ++- 4 files changed, 136 insertions(+), 126 deletions(-) diff --git a/apps/docs/src/app/docs/api/senate-house/page.mdx b/apps/docs/src/app/docs/api/senate-house/page.mdx index f6ffefb..c560efb 100644 --- a/apps/docs/src/app/docs/api/senate-house/page.mdx +++ b/apps/docs/src/app/docs/api/senate-house/page.mdx @@ -8,32 +8,32 @@ The Senate & House Trading endpoints provide access to congressional trading dat endpoints={[ { method: 'GET', - path: '/senate-trading', + path: '/stable/senate-trades', description: 'Get senate trading data for a specific stock symbol', }, { method: 'GET', - path: '/senate-trading-rss-feed', + path: '/stable/senate-latest', description: 'Get senate trading RSS feed with pagination', }, { method: 'GET', - path: '/senate-trades-by-name', + path: '/stable/senate-trades-by-name', description: 'Get senate trading data by senator name', }, { method: 'GET', - path: '/senate-disclosure', + path: '/stable/house-trades', description: 'Get house trading data for a specific stock symbol', }, { method: 'GET', - path: '/senate-disclosure-rss-feed', + path: '/stable/house-latest', description: 'Get house trading RSS feed with pagination', }, { method: 'GET', - path: '/house-trades-by-name', + path: '/stable/house-trades-by-name', description: 'Get house trading data by representative name', }, ]} @@ -69,19 +69,21 @@ Retrieve senate trading data for a specific stock symbol. success: true, data: [ { - firstName: "John", - lastName: "Doe", - office: "Senator", - link: "https://disclosures-clerk.house.gov/...", - dateRecieved: "2024-01-15", - transactionDate: "2024-01-10", - owner: "Self", - assetDescription: "Apple Inc. Common Stock", - assetType: "Stock", - type: "Purchase", - amount: "$1,001 - $15,000", - comment: "Purchase of Apple stock", - symbol: "AAPL" + "symbol": "AAPL", + "disclosureDate": "2014-02-27", + "transactionDate": "2014-02-10", + "firstName": "Sheldon", + "lastName": "Whitehouse", + "office": "Sheldon Whitehouse", + "district": "RI", + "owner": "Joint", + "assetDescription": "Apple Inc. (NASDAQ)", + "assetType": "", + "type": "Sale (Partial)", + "amount": "$15,001 - $50,000", + "capitalGainsOver200USD": "False", + "comment": "--", + "link": "https://efdsearch.senate.gov/search/view/ptr/1237c9ae-5171-4e3c-aa4d-41bd4789ce8d/" } ] }`} @@ -107,6 +109,12 @@ Retrieve senate trading data through RSS feed with pagination. required: true, description: 'Page number for pagination (0-based)', }, + { + name: 'limit', + type: 'number', + required: false, + description: 'Number of results (default 100)', + }, ]} /> @@ -117,19 +125,21 @@ Retrieve senate trading data through RSS feed with pagination. success: true, data: [ { - firstName: "Jane", - lastName: "Smith", - office: "Senator", - link: "https://disclosures-clerk.house.gov/...", - dateRecieved: "2024-01-20", - transactionDate: "2024-01-15", - owner: "Spouse", - assetDescription: "Microsoft Corporation Common Stock", - assetType: "Stock", - type: "Sale", - amount: "$15,001 - $50,000", - comment: "Sale of Microsoft stock", - symbol: "MSFT" + "symbol": "AAPL", + "disclosureDate": "2014-02-27", + "transactionDate": "2014-02-10", + "firstName": "Sheldon", + "lastName": "Whitehouse", + "office": "Sheldon Whitehouse", + "district": "RI", + "owner": "Joint", + "assetDescription": "Apple Inc. (NASDAQ)", + "assetType": "", + "type": "Sale (Partial)", + "amount": "$15,001 - $50,000", + "capitalGainsOver200USD": "False", + "comment": "--", + "link": "https://efdsearch.senate.gov/search/view/ptr/1237c9ae-5171-4e3c-aa4d-41bd4789ce8d/" } ] }`} @@ -215,18 +225,21 @@ Retrieve house trading data for a specific stock symbol. success: true, data: [ { - disclosureYear: "2024", - disclosureDate: "2024-01-15", - transactionDate: "2024-01-10", - owner: "Self", - ticker: "GOOGL", - assetDescription: "Alphabet Inc. Class C Capital Stock", - type: "Purchase", - amount: "$1,001 - $15,000", - representative: "John Representative", - district: "CA-12", - link: "https://disclosures-clerk.house.gov/...", - capitalGainsOver200USD: "No" + "symbol": "AAPL", + "disclosureDate": "2018-07-09", + "transactionDate": "2017-06-27", + "firstName": "K. Michael", + "lastName": "Conaway", + "office": "K. Michael Conaway", + "district": "TX11", + "owner": "", + "assetDescription": "Apple Inc.", + "assetType": "", + "type": "Purchase", + "amount": "$15,001 - $50,000", + "capitalGainsOver200USD": "False", + "comment": "", + "link": "https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/2018/20009819.pdf" } ] }`} @@ -252,6 +265,12 @@ Retrieve house trading data through RSS feed with pagination. required: true, description: 'Page number for pagination (0-based)', }, + { + name: 'limit', + type: 'number', + required: false, + description: 'Number of results (default 100)', + }, ]} /> @@ -262,18 +281,21 @@ Retrieve house trading data through RSS feed with pagination. success: true, data: [ { - disclosureYear: "2024", - disclosureDate: "2024-01-20", - transactionDate: "2024-01-15", - owner: "Spouse", - ticker: "TSLA", - assetDescription: "Tesla, Inc. Common Stock", - type: "Sale", - amount: "$15,001 - $50,000", - representative: "Jane Representative", - district: "NY-14", - link: "https://disclosures-clerk.house.gov/...", - capitalGainsOver200USD: "Yes" + "symbol": "AAPL", + "disclosureDate": "2018-07-09", + "transactionDate": "2017-06-27", + "firstName": "K. Michael", + "lastName": "Conaway", + "office": "K. Michael Conaway", + "district": "TX11", + "owner": "", + "assetDescription": "Apple Inc.", + "assetType": "", + "type": "Purchase", + "amount": "$15,001 - $50,000", + "capitalGainsOver200USD": "False", + "comment": "", + "link": "https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/2018/20009819.pdf" } ] }`} @@ -337,19 +359,21 @@ Retrieve house trading data for a specific representative by name. {`interface SenateTradingResponse { + symbol: string; + disclosureDate: string; + transactionDate: string; firstName: string; lastName: string; office: string; - link: string; - dateRecieved: string; - transactionDate: string; + district: string; owner: string; assetDescription: string; assetType: string; type: string; amount: string; + capitalGainsOver200USD: string; comment: string; - symbol: string; + link: string; }`} @@ -357,18 +381,21 @@ Retrieve house trading data for a specific representative by name. {`interface HouseTradingResponse { - disclosureYear: string; + symbol: string; disclosureDate: string; transactionDate: string; + firstName: string; + lastName: string; + office: string; + district: string; owner: string; - ticker: string; assetDescription: string; + assetType: string; type: string; amount: string; - representative: string; - district: string; - link: string; capitalGainsOver200USD: string; + comment: string; + link: string; }`} diff --git a/packages/api/src/__tests__/endpoints/senate-house.test.ts b/packages/api/src/__tests__/endpoints/senate-house.test.ts index 0842631..7b10981 100644 --- a/packages/api/src/__tests__/endpoints/senate-house.test.ts +++ b/packages/api/src/__tests__/endpoints/senate-house.test.ts @@ -1,13 +1,10 @@ import { FMP } from '../../fmp'; -// Mock API key for testing -const API_KEY = 'testapikey123456789012345678901234567890'; - describe('SenateHouseEndpoints', () => { let fmp: FMP; beforeEach(() => { - fmp = new FMP({ apiKey: API_KEY }); + fmp = new FMP(); }); describe('getSenateTrading', () => { @@ -30,7 +27,7 @@ describe('SenateHouseEndpoints', () => { expect(firstItem).toHaveProperty('firstName'); expect(firstItem).toHaveProperty('lastName'); expect(firstItem).toHaveProperty('office'); - expect(firstItem).toHaveProperty('dateRecieved'); + expect(firstItem).toHaveProperty('disclosureDate'); expect(firstItem).toHaveProperty('transactionDate'); expect(firstItem).toHaveProperty('owner'); expect(firstItem).toHaveProperty('assetDescription'); @@ -39,11 +36,7 @@ describe('SenateHouseEndpoints', () => { expect(firstItem).toHaveProperty('amount'); expect(firstItem).toHaveProperty('comment'); expect(firstItem).toHaveProperty('symbol'); - // Should NOT have HouseTradingResponse specific fields - expect(firstItem).not.toHaveProperty('disclosureYear'); - expect(firstItem).not.toHaveProperty('representative'); - expect(firstItem).not.toHaveProperty('district'); - expect(firstItem).not.toHaveProperty('capitalGainsOver200USD'); + // Note: Both Senate and House now have similar fields in unified structure } } }); @@ -62,6 +55,7 @@ describe('SenateHouseEndpoints', () => { it('should get senate trading RSS feed for page 0', async () => { const result = await fmp.senateHouse.getSenateTradingRSSFeed({ page: 0, + limit: 5, }); expect(result).toBeDefined(); @@ -78,11 +72,8 @@ describe('SenateHouseEndpoints', () => { expect(firstItem).toHaveProperty('firstName'); expect(firstItem).toHaveProperty('lastName'); expect(firstItem).toHaveProperty('office'); - expect(firstItem).toHaveProperty('dateRecieved'); - // Should NOT have HouseTradingResponse specific fields - expect(firstItem).not.toHaveProperty('disclosureYear'); - expect(firstItem).not.toHaveProperty('representative'); - expect(firstItem).not.toHaveProperty('district'); + expect(firstItem).toHaveProperty('disclosureDate'); + // Note: Both Senate and House now have similar fields in unified structure } } }); @@ -123,24 +114,19 @@ describe('SenateHouseEndpoints', () => { if (houseData.length > 0) { const firstItem = houseData[0]; // Check for HouseTradingResponse specific fields - expect(firstItem).toHaveProperty('disclosureYear'); expect(firstItem).toHaveProperty('disclosureDate'); expect(firstItem).toHaveProperty('transactionDate'); expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('ticker'); + expect(firstItem).toHaveProperty('symbol'); expect(firstItem).toHaveProperty('assetDescription'); expect(firstItem).toHaveProperty('type'); expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('representative'); + expect(firstItem).toHaveProperty('firstName'); + expect(firstItem).toHaveProperty('lastName'); expect(firstItem).toHaveProperty('district'); expect(firstItem).toHaveProperty('link'); expect(firstItem).toHaveProperty('capitalGainsOver200USD'); - // Should NOT have SenateTradingResponse specific fields - expect(firstItem).not.toHaveProperty('firstName'); - expect(firstItem).not.toHaveProperty('lastName'); - expect(firstItem).not.toHaveProperty('office'); - expect(firstItem).not.toHaveProperty('dateRecieved'); - expect(firstItem).not.toHaveProperty('comment'); + // Note: Both Senate and House now have office field in unified structure } } }); @@ -159,6 +145,7 @@ describe('SenateHouseEndpoints', () => { it('should get house trading RSS feed for page 0', async () => { const result = await fmp.senateHouse.getHouseTradingRSSFeed({ page: 0, + limit: 5, }); expect(result).toBeDefined(); @@ -173,15 +160,12 @@ describe('SenateHouseEndpoints', () => { if (houseData.length > 0) { const firstItem = houseData[0]; // Check for HouseTradingResponse specific fields - expect(firstItem).toHaveProperty('disclosureYear'); expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('representative'); + expect(firstItem).toHaveProperty('firstName'); + expect(firstItem).toHaveProperty('lastName'); expect(firstItem).toHaveProperty('district'); expect(firstItem).toHaveProperty('capitalGainsOver200USD'); - // Should NOT have SenateTradingResponse specific fields - expect(firstItem).not.toHaveProperty('firstName'); - expect(firstItem).not.toHaveProperty('lastName'); - expect(firstItem).not.toHaveProperty('office'); + // Note: Both Senate and House now have office field in unified structure } } }); @@ -220,7 +204,6 @@ describe('SenateHouseEndpoints', () => { expect(Array.isArray(senateData)).toBe(true); if (senateData.length > 0) { - console.log(`✅ Senate trading by name "Jerry" returned ${senateData.length} records`); const firstItem = senateData[0]; // Check for SenateHouseTradingByNameResponse specific fields expect(firstItem).toHaveProperty('symbol'); @@ -238,8 +221,6 @@ describe('SenateHouseEndpoints', () => { expect(firstItem).toHaveProperty('capitalGainsOver200USD'); expect(firstItem).toHaveProperty('comment'); expect(firstItem).toHaveProperty('link'); - } else { - console.log('⚠️ Senate trading by name "Jerry" returned empty array'); } } }); @@ -255,9 +236,9 @@ describe('SenateHouseEndpoints', () => { if (result.success && result.data) { expect(Array.isArray(result.data)).toBe(true); if (result.data.length > 0) { - console.log(`✅ Senate trading by name "John" returned ${result.data.length} records`); + // Data found as expected } else { - console.log('⚠️ Senate trading by name "John" returned empty array'); + // No data found as expected } } }); @@ -274,7 +255,6 @@ describe('SenateHouseEndpoints', () => { if (result.success && result.data) { expect(Array.isArray(result.data)).toBe(true); expect(result.data.length).toBe(0); - console.log('✅ Non-existent name correctly returned empty array'); } }); }); @@ -294,7 +274,6 @@ describe('SenateHouseEndpoints', () => { expect(Array.isArray(houseData)).toBe(true); if (houseData.length > 0) { - console.log(`✅ House trading by name "Nancy" returned ${houseData.length} records`); const firstItem = houseData[0]; // Check for SenateHouseTradingByNameResponse specific fields expect(firstItem).toHaveProperty('symbol'); @@ -312,8 +291,6 @@ describe('SenateHouseEndpoints', () => { expect(firstItem).toHaveProperty('capitalGainsOver200USD'); expect(firstItem).toHaveProperty('comment'); expect(firstItem).toHaveProperty('link'); - } else { - console.log('⚠️ House trading by name "Nancy" returned empty array'); } } }); @@ -329,9 +306,9 @@ describe('SenateHouseEndpoints', () => { if (result.success && result.data) { expect(Array.isArray(result.data)).toBe(true); if (result.data.length > 0) { - console.log(`✅ House trading by name "Kevin" returned ${result.data.length} records`); + // Data found as expected } else { - console.log('⚠️ House trading by name "Kevin" returned empty array'); + // No data found as expected } } }); @@ -348,7 +325,6 @@ describe('SenateHouseEndpoints', () => { if (result.success && result.data) { expect(Array.isArray(result.data)).toBe(true); expect(result.data.length).toBe(0); - console.log('✅ Non-existent name correctly returned empty array'); } }); }); diff --git a/packages/api/src/endpoints/senate-house.ts b/packages/api/src/endpoints/senate-house.ts index bdbf570..4410f5a 100644 --- a/packages/api/src/endpoints/senate-house.ts +++ b/packages/api/src/endpoints/senate-house.ts @@ -37,13 +37,13 @@ export class SenateHouseEndpoints { * const teslaSenateTrading = await fmp.senateHouse.getSenateTrading({ symbol: 'TSLA' }); * ``` * - * @see {@link https://site.financialmodelingprep.com/developer/docs#senate-trading|FMP Senate Trading Documentation} + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#senate-trading|FMP Senate Trading Documentation} */ async getSenateTrading(params: { symbol: string; }): Promise> { const { symbol } = params; - return this.client.get(`/senate-trading`, 'v4', { symbol }); + return this.client.get(`/senate-trades`, 'stable', { symbol }); } /** @@ -75,13 +75,14 @@ export class SenateHouseEndpoints { * }); * ``` * - * @see {@link https://site.financialmodelingprep.com/developer/docs#senate-trading-rss-feed-senate|FMP Senate Trading RSS Feed Documentation} + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#senate-latest|FMP Senate Trading RSS Feed Documentation} */ async getSenateTradingRSSFeed(params: { page: number; + limit?: number; }): Promise> { - const { page } = params; - return this.client.get('/senate-trading-rss-feed', 'v4', { page }); + const { page, limit } = params; + return this.client.get('/senate-latest', 'stable', { page, limit: limit ?? 100 }); } /** @@ -147,12 +148,12 @@ export class SenateHouseEndpoints { * console.log(`Microsoft house trading activities: ${msftHouseTrading.data.length}`); * ``` * - * @see {@link https://site.financialmodelingprep.com/developer/docs#house-disclosure|FMP House Trading Documentation} + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#house-trading|FMP House Trading Documentation} */ async getHouseTrading(params: { symbol: string }): Promise> { const { symbol } = params; - return this.client.get('/senate-disclosure', 'v4', { symbol }); + return this.client.get('/house-trades', 'stable', { symbol }); } /** @@ -184,13 +185,14 @@ export class SenateHouseEndpoints { * }); * ``` * - * @see {@link https://site.financialmodelingprep.com/developer/docs#house-disclosure-rss-feed-senate|FMP House Trading RSS Feed Documentation} + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#house-latest|FMP House Trading RSS Feed Documentation} */ async getHouseTradingRSSFeed(params: { page: number; + limit?: number; }): Promise> { - const { page } = params; - return this.client.get('/senate-disclosure-rss-feed', 'v4', { page }); + const { page, limit } = params; + return this.client.get('/house-latest', 'stable', { page, limit: limit ?? 100 }); } /** diff --git a/packages/types/src/senate-house.ts b/packages/types/src/senate-house.ts index fc30a58..8c23fdf 100644 --- a/packages/types/src/senate-house.ts +++ b/packages/types/src/senate-house.ts @@ -2,35 +2,40 @@ // Senate trading response interface export interface SenateTradingResponse { + symbol: string; + disclosureDate: string; + transactionDate: string; firstName: string; lastName: string; office: string; - link: string; - dateRecieved: string; - transactionDate: string; + district: string; owner: string; assetDescription: string; assetType: string; type: string; amount: string; + capitalGainsOver200USD: string; comment: string; - symbol: string; + link: string; } // House trading response interface export interface HouseTradingResponse { - disclosureYear: string; + symbol: string; disclosureDate: string; transactionDate: string; + firstName: string; + lastName: string; + office: string; + district: string; owner: string; - ticker: string; assetDescription: string; + assetType: string; type: string; amount: string; - representative: string; - district: string; - link: string; capitalGainsOver200USD: string; + comment: string; + link: string; } export interface SenateHouseTradingByNameResponse { From cbcff85d890800b4064210365aff3fa1d0941353 Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 11 Sep 2025 12:06:32 -0400 Subject: [PATCH 19/21] feat: add screener endpoint for api wrapper --- apps/docs/src/app/docs/api/layout.tsx | 1 + apps/docs/src/app/docs/api/screener/page.mdx | 600 ++++++++++++++++++ apps/docs/src/app/page.tsx | 17 +- packages/api/scripts/test-endpoint.ts | 19 + .../src/__tests__/endpoints/screener.test.ts | 379 +++++++++++ .../__tests__/endpoints/senate-house.test.ts | 599 +++++++++-------- packages/api/src/endpoints/screener.ts | 181 ++++++ packages/api/src/fmp.ts | 64 +- packages/types/src/index.ts | 51 +- packages/types/src/screener.ts | 60 ++ 10 files changed, 1640 insertions(+), 331 deletions(-) create mode 100644 apps/docs/src/app/docs/api/screener/page.mdx create mode 100644 packages/api/src/__tests__/endpoints/screener.test.ts create mode 100644 packages/api/src/endpoints/screener.ts create mode 100644 packages/types/src/screener.ts diff --git a/apps/docs/src/app/docs/api/layout.tsx b/apps/docs/src/app/docs/api/layout.tsx index 4f9e8ea..fe30d6b 100644 --- a/apps/docs/src/app/docs/api/layout.tsx +++ b/apps/docs/src/app/docs/api/layout.tsx @@ -39,6 +39,7 @@ const apiNavigationGroups = [ { name: 'List Endpoints', href: '/docs/api/list' }, { name: 'Calendar Endpoints', href: '/docs/api/calendar' }, { name: 'Company Endpoints', href: '/docs/api/company' }, + { name: 'Screener Endpoints', href: '/docs/api/screener' }, { name: 'Senate & House Trading', href: '/docs/api/senate-house' }, { name: 'Institutional Endpoints', href: '/docs/api/institutional' }, { name: 'Insider Endpoints', href: '/docs/api/insider' }, diff --git a/apps/docs/src/app/docs/api/screener/page.mdx b/apps/docs/src/app/docs/api/screener/page.mdx new file mode 100644 index 0000000..b630504 --- /dev/null +++ b/apps/docs/src/app/docs/api/screener/page.mdx @@ -0,0 +1,600 @@ +# Screener Endpoints + +The Screener Endpoints provide powerful tools for filtering and searching companies based on various financial metrics, market data, and company characteristics. Perfect for finding investment opportunities, conducting market research, or building custom stock filters. + +## Available Methods + + + +## Get Company Screener + +Screen companies based on customizable financial criteria. This endpoint allows you to filter and search for companies based on various financial metrics, market data, and company characteristics. + + + {`const companies = await fmp.screener.getScreener({ + marketCapMoreThan: 1000000000, // $1B+ + sector: 'Technology', + isActivelyTrading: true, + limit: 50 +});`} + + + + +### Example Response + + + {`{ + success: true, + data: [ + { + symbol: 'AAPL', + companyName: 'Apple Inc.', + marketCap: 2375000000000, + sector: 'Technology', + industry: 'Consumer Electronics', + beta: 1.28, + price: 150.25, + lastAnnualDividend: 0.24, + volume: 52345600, + exchange: 'NASDAQ', + exchangeShortName: 'NASDAQ', + country: 'US', + isEtf: false, + isFund: false, + isActivelyTrading: true + }, + { + symbol: 'MSFT', + companyName: 'Microsoft Corporation', + marketCap: 2100000000000, + sector: 'Technology', + industry: 'Software—Infrastructure', + beta: 0.95, + price: 280.15, + lastAnnualDividend: 0.68, + volume: 23456700, + exchange: 'NASDAQ', + exchangeShortName: 'NASDAQ', + country: 'US', + isEtf: false, + isFund: false, + isActivelyTrading: true + } + ] +}`} + + +## Get Available Exchanges + +Retrieve all supported stock exchanges that can be used as filters in the company screener. + + + {`const exchanges = await fmp.screener.getAvailableExchanges();`} + + +### Example Response + + + {`{ + success: true, + data: [ + { + exchange: 'NASDAQ', + name: 'NASDAQ', + countryName: 'United States', + countryCode: 'US', + symbolSuffix: '', + delay: '15' + }, + { + exchange: 'NYSE', + name: 'New York Stock Exchange', + countryName: 'United States', + countryCode: 'US', + symbolSuffix: '', + delay: '15' + }, + { + exchange: 'LSE', + name: 'London Stock Exchange', + countryName: 'United Kingdom', + countryCode: 'GB', + symbolSuffix: '.L', + delay: '15' + } + ] +}`} + + +## Get Available Sectors + +Retrieve all supported business sectors that can be used as filters in the company screener. + + + {`const sectors = await fmp.screener.getAvailableSectors();`} + + +### Example Response + + + {`{ + success: true, + data: [ + { + sector: 'Technology' + }, + { + sector: 'Healthcare' + }, + { + sector: 'Financial Services' + }, + { + sector: 'Consumer Cyclical' + }, + { + sector: 'Industrials' + } + ] +}`} + + +## Get Available Industries + +Retrieve all supported industries that can be used as filters in the company screener. Provides more granular filtering than sectors. + + + {`const industries = await fmp.screener.getAvailableIndustries();`} + + +### Example Response + + + {`{ + success: true, + data: [ + { + industry: 'Software—Application' + }, + { + industry: 'Software—Infrastructure' + }, + { + industry: 'Consumer Electronics' + }, + { + industry: 'Pharmaceuticals' + }, + { + industry: 'Banks—Diversified' + } + ] +}`} + + +## Get Available Countries + +Retrieve all supported countries that can be used as filters in the company screener. + + + {`const countries = await fmp.screener.getAvailableCountries();`} + + +### Example Response + + + {`{ + success: true, + data: [ + { + country: 'US' + }, + { + country: 'CA' + }, + { + country: 'GB' + }, + { + country: 'DE' + }, + { + country: 'JP' + } + ] +}`} + + +## Data Types + +### Screener + + + {`interface Screener { + symbol: string; + companyName: string; + marketCap: number; + sector: string; + industry: string; + beta: number; + price: number; + lastAnnualDividend: number; + volume: number; + exchange: string; + exchangeShortName: string; + country: string; + isEtf: boolean; + isFund: boolean; + isActivelyTrading: boolean; +}`} + + +### AvailableExchanges + + + {`interface AvailableExchanges { + exchange: string; + name: string; + countryName: string; + countryCode: string; + symbolSuffix: string; + delay: string; +}`} + + +### AvailableSectors + + + {`interface AvailableSectors { + sector: string; +}`} + + +### AvailableIndustries + + + {`interface AvailableIndustries { + industry: string; +}`} + + +### AvailableCountries + + + {`interface AvailableCountries { + country: string; +}`} + + +### ScreenerParams + + + {`interface ScreenerParams { + marketCapMoreThan?: number; + marketCapLowerThan?: number; + sector?: string; + industry?: string; + betaMoreThan?: number; + betaLowerThan?: number; + priceMoreThan?: number; + priceLowerThan?: number; + dividendMoreThan?: number; + dividendLowerThan?: number; + volumeMoreThan?: number; + volumeLowerThan?: number; + exchange?: string; + country?: string; + isEtf?: boolean; + isFund?: boolean; + isActivelyTrading?: boolean; + limit?: number; + includeAllShareClasses?: boolean; +}`} + + +## Error Handling + +All screener endpoints return a standardized response format with error handling: + + + {`interface UnwrappedAPIResponse { + success: boolean; + data: T | null; + error?: string; +}`} + + +When an error occurs, the response will include: + +- `success: false` +- `data: null` +- `error: string` - Description of the error + +## Rate Limits + +Screener endpoints are subject to the same rate limits as other FMP API endpoints. Please refer to your subscription plan for specific limits. + +## Examples + +### Basic Company Screening + + + {`// Find large-cap tech stocks +const techStocks = await fmp.screener.getScreener({ + marketCapMoreThan: 10000000000, // $10B+ + sector: 'Technology', + isActivelyTrading: true, + limit: 50 +}); + +// Find dividend-paying stocks +const dividendStocks = await fmp.screener.getScreener({ +dividendMoreThan: 0.03, // 3%+ dividend yield +marketCapMoreThan: 1000000000, // $1B+ market cap +isActivelyTrading: true +}); + +// Find small-cap value stocks +const smallCapValue = await fmp.screener.getScreener({ +marketCapLowerThan: 2000000000, // Under $2B +priceMoreThan: 5, // Above $5 +betaLowerThan: 1.2, // Lower volatility +limit: 100 +});`} + + + +### Dynamic Filtering + + + {`// Get available sectors first +const sectorsResult = await fmp.screener.getAvailableSectors(); +if (sectorsResult.success && sectorsResult.data) { + const healthcareSector = sectorsResult.data.find(s => s.sector === 'Healthcare'); + + if (healthcareSector) { + // Use the sector in screening + const healthcareStocks = await fmp.screener.getScreener({ + sector: healthcareSector.sector, + marketCapMoreThan: 5000000000, + isActivelyTrading: true, + limit: 25 + }); + } +}`} + + +### Multi-Criteria Analysis + + + {`// Complex screening with multiple criteria +const growthStocks = await fmp.screener.getScreener({ + marketCapMoreThan: 1000000000, // $1B+ market cap + marketCapLowerThan: 100000000000, // Under $100B + priceMoreThan: 10, // Above $10 + betaMoreThan: 1.0, // Higher volatility + volumeMoreThan: 1000000, // High volume + isActivelyTrading: true, + limit: 100 +}); + +// Analyze results +if (growthStocks.success && growthStocks.data) { +console.log('Found ' + growthStocks.data.length + ' growth stocks'); + +const avgMarketCap = growthStocks.data.reduce((sum, stock) => +sum + stock.marketCap, 0) / growthStocks.data.length; + +console.log('Average market cap:', avgMarketCap); +}`} + + + +### Sector Analysis + + + {`// Analyze different sectors +const sectors = ['Technology', 'Healthcare', 'Financial Services']; + +for (const sector of sectors) { +const sectorStocks = await fmp.screener.getScreener({ +sector: sector, +marketCapMoreThan: 10000000000, // $10B+ +isActivelyTrading: true, +limit: 20 +}); + +if (sectorStocks.success && sectorStocks.data) { +console.log(sector + ': ' + sectorStocks.data.length + ' companies'); + + const avgPrice = sectorStocks.data.reduce((sum, stock) => + sum + stock.price, 0) / sectorStocks.data.length; + + console.log('Average price: $' + avgPrice.toFixed(2)); + +} +}`} + + + +### Portfolio Screening + + + {`// Build a diversified portfolio +const portfolioCriteria = [ + { + name: 'Large Cap Tech', + criteria: { + sector: 'Technology', + marketCapMoreThan: 50000000000, // $50B+ + isActivelyTrading: true, + limit: 5 + } + }, + { + name: 'Dividend Stocks', + criteria: { + dividendMoreThan: 0.02, // 2%+ dividend + marketCapMoreThan: 5000000000, // $5B+ + isActivelyTrading: true, + limit: 5 + } + }, + { + name: 'Growth Stocks', + criteria: { + marketCapMoreThan: 1000000000, // $1B+ + marketCapLowerThan: 20000000000, // Under $20B + betaMoreThan: 1.2, // Higher volatility + isActivelyTrading: true, + limit: 5 + } + } +]; + +const portfolio = {}; + +for (const category of portfolioCriteria) { +const result = await fmp.screener.getScreener(category.criteria); +if (result.success && result.data) { +portfolio[category.name] = result.data; +} +} + +console.log('Portfolio built:', Object.keys(portfolio));`} + + diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx index 094d99a..9dd6b87 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -18,19 +18,13 @@ export default function Home() {

FMP Node Wrapper

-

- Choose the library that fits your needs -

A comprehensive Node.js ecosystem for the Financial Modeling Prep API

- - Financial Modeling Prep API Key - - Link for 10% off + Financial Modeling Prep API Key - Link for 10% off @@ -38,10 +32,17 @@ export default function Home() { href="https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy" target="_blank" rel="noopener noreferrer" - className="text-lg text-neutral-500 dark:text-neutral-400 hover:underline" + className="text-lg text-blue-500 dark:text-blue-400 hover:underline" > https://site.financialmodelingprep.com/pricing-plans?couponCode=eroy +
+ I don't get paid for the working on this project. Using this link helps support the + project with affiliate earnings. +
+
+ If this project helps you, consider giving it a star on GitHub. +
diff --git a/packages/api/scripts/test-endpoint.ts b/packages/api/scripts/test-endpoint.ts index 409a11e..612f345 100644 --- a/packages/api/scripts/test-endpoint.ts +++ b/packages/api/scripts/test-endpoint.ts @@ -511,6 +511,25 @@ async function testEndpoint() { }); break; + // Screener endpoints + case 'screener': + result = await fmp.screener.getScreener({ + limit: 10, + }); + break; + case 'available-exchanges': + result = await fmp.screener.getAvailableExchanges(); + break; + case 'available-sectors': + result = await fmp.screener.getAvailableSectors(); + break; + case 'available-industries': + result = await fmp.screener.getAvailableIndustries(); + break; + case 'available-countries': + result = await fmp.screener.getAvailableCountries(); + break; + // Stock endpoints case 'market-cap': result = await fmp.stock.getMarketCap('AAPL'); diff --git a/packages/api/src/__tests__/endpoints/screener.test.ts b/packages/api/src/__tests__/endpoints/screener.test.ts new file mode 100644 index 0000000..87fb79c --- /dev/null +++ b/packages/api/src/__tests__/endpoints/screener.test.ts @@ -0,0 +1,379 @@ +import { FMP } from '../../fmp'; +import { shouldSkipTests, createTestClient, API_TIMEOUT, FAST_TIMEOUT } from '../utils/test-setup'; + +describe('Screener Endpoints', () => { + let fmp: FMP; + + beforeAll(() => { + if (shouldSkipTests()) { + console.log('Skipping screener tests - no API key available'); + return; + } + fmp = createTestClient(); + }); + + describe('getScreener', () => { + it( + 'should fetch companies with basic screening criteria', + async () => { + if (shouldSkipTests()) { + console.log('Skipping screener test - no API key available'); + return; + } + const result = await fmp.screener.getScreener({ + marketCapMoreThan: 1000000000, // $1B+ + isActivelyTrading: true, + limit: 10, + }); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + expect(result.data.length).toBeGreaterThan(0); + expect(result.data.length).toBeLessThanOrEqual(10); + + const company = result.data[0]; + expect(company.symbol).toBeDefined(); + expect(company.companyName).toBeDefined(); + expect(company.marketCap).toBeGreaterThan(1000000000); + expect(company.price).toBeGreaterThan(0); + expect(company.sector).toBeDefined(); + expect(company.industry).toBeDefined(); + expect(company.exchange).toBeDefined(); + } + }, + API_TIMEOUT, + ); + + it( + 'should fetch tech sector companies', + async () => { + if (shouldSkipTests()) { + console.log('Skipping tech sector screener test - no API key available'); + return; + } + const result = await fmp.screener.getScreener({ + sector: 'Technology', + marketCapMoreThan: 5000000000, // $5B+ + isActivelyTrading: true, + limit: 5, + }); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + expect(result.data.length).toBeGreaterThan(0); + expect(result.data.length).toBeLessThanOrEqual(5); + + const company = result.data[0]; + expect(company.symbol).toBeDefined(); + expect(company.sector).toBe('Technology'); + expect(company.marketCap).toBeGreaterThan(5000000000); + } + }, + API_TIMEOUT, + ); + + it( + 'should fetch dividend-paying stocks', + async () => { + if (shouldSkipTests()) { + console.log('Skipping dividend screener test - no API key available'); + return; + } + const result = await fmp.screener.getScreener({ + dividendMoreThan: 0.02, // 2%+ dividend yield + marketCapMoreThan: 2000000000, // $2B+ + isActivelyTrading: true, + limit: 5, + }); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + expect(result.data.length).toBeGreaterThan(0); + expect(result.data.length).toBeLessThanOrEqual(5); + + const company = result.data[0]; + expect(company.symbol).toBeDefined(); + expect(company.lastAnnualDividend).toBeGreaterThan(0.02); + expect(company.marketCap).toBeGreaterThan(2000000000); + } + }, + API_TIMEOUT, + ); + + it( + 'should handle empty results gracefully', + async () => { + if (shouldSkipTests()) { + console.log('Skipping empty results screener test - no API key available'); + return; + } + const result = await fmp.screener.getScreener({ + marketCapMoreThan: 999999999999999, // Unrealistically high market cap + isActivelyTrading: true, + limit: 10, + }); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBe(0); + }, + FAST_TIMEOUT, + ); + + it( + 'should handle invalid parameters gracefully', + async () => { + if (shouldSkipTests()) { + console.log('Skipping invalid parameters screener test - no API key available'); + return; + } + const result = await fmp.screener.getScreener({ + marketCapMoreThan: -1000, // Invalid negative value + isActivelyTrading: true, + limit: 5, + }); + + // Should either return empty results or handle gracefully + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + }, + FAST_TIMEOUT, + ); + }); + + describe('getAvailableExchanges', () => { + it( + 'should fetch available exchanges', + async () => { + if (shouldSkipTests()) { + console.log('Skipping available exchanges test - no API key available'); + return; + } + const result = await fmp.screener.getAvailableExchanges(); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + expect(result.data.length).toBeGreaterThan(0); + + const exchange = result.data[0]; + expect(exchange.exchange).toBeDefined(); + expect(exchange.name).toBeDefined(); + expect(exchange.countryName).toBeDefined(); + expect(exchange.countryCode).toBeDefined(); + } + }, + FAST_TIMEOUT, + ); + + it( + 'should contain common exchanges', + async () => { + if (shouldSkipTests()) { + console.log('Skipping common exchanges test - no API key available'); + return; + } + const result = await fmp.screener.getAvailableExchanges(); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + const exchangeNames = result.data.map(ex => ex.exchange); + expect(exchangeNames).toContain('NASDAQ'); + expect(exchangeNames).toContain('NYSE'); + } + }, + FAST_TIMEOUT, + ); + }); + + describe('getAvailableSectors', () => { + it( + 'should fetch available sectors', + async () => { + if (shouldSkipTests()) { + console.log('Skipping available sectors test - no API key available'); + return; + } + const result = await fmp.screener.getAvailableSectors(); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + expect(result.data.length).toBeGreaterThan(0); + + const sector = result.data[0]; + expect(sector.sector).toBeDefined(); + } + }, + FAST_TIMEOUT, + ); + + it( + 'should contain common sectors', + async () => { + if (shouldSkipTests()) { + console.log('Skipping common sectors test - no API key available'); + return; + } + const result = await fmp.screener.getAvailableSectors(); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + const sectors = result.data.map(s => s.sector); + expect(sectors).toContain('Technology'); + expect(sectors).toContain('Healthcare'); + expect(sectors).toContain('Financial Services'); + } + }, + FAST_TIMEOUT, + ); + }); + + describe('getAvailableIndustries', () => { + it( + 'should fetch available industries', + async () => { + if (shouldSkipTests()) { + console.log('Skipping available industries test - no API key available'); + return; + } + const result = await fmp.screener.getAvailableIndustries(); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + expect(result.data.length).toBeGreaterThan(0); + + const industry = result.data[0]; + expect(industry.industry).toBeDefined(); + } + }, + FAST_TIMEOUT, + ); + + it( + 'should contain tech-related industries', + async () => { + if (shouldSkipTests()) { + console.log('Skipping tech industries test - no API key available'); + return; + } + const result = await fmp.screener.getAvailableIndustries(); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + const industries = result.data.map(i => i.industry); + const hasSoftwareIndustry = industries.some(industry => + industry.toLowerCase().includes('software'), + ); + expect(hasSoftwareIndustry).toBe(true); + } + }, + FAST_TIMEOUT, + ); + }); + + describe('getAvailableCountries', () => { + it( + 'should fetch available countries', + async () => { + if (shouldSkipTests()) { + console.log('Skipping available countries test - no API key available'); + return; + } + const result = await fmp.screener.getAvailableCountries(); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + expect(result.data.length).toBeGreaterThan(0); + + const country = result.data[0]; + expect(country.country).toBeDefined(); + } + }, + FAST_TIMEOUT, + ); + + it( + 'should contain major countries', + async () => { + if (shouldSkipTests()) { + console.log('Skipping major countries test - no API key available'); + return; + } + const result = await fmp.screener.getAvailableCountries(); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + + if (result.data && Array.isArray(result.data)) { + const countries = result.data.map(c => c.country); + expect(countries).toContain('US'); + expect(countries).toContain('CA'); + } + }, + FAST_TIMEOUT, + ); + }); + + describe('Integration Tests', () => { + it( + 'should use available data in screener filters', + async () => { + if (shouldSkipTests()) { + console.log('Skipping integration test - no API key available'); + return; + } + + // First get available sectors + const sectorsResult = await fmp.screener.getAvailableSectors(); + expect(sectorsResult.success).toBe(true); + expect(sectorsResult.data).toBeDefined(); + + if ( + sectorsResult.data && + Array.isArray(sectorsResult.data) && + sectorsResult.data.length > 0 + ) { + const firstSector = sectorsResult.data[0].sector; + + // Use that sector in a screener query + const screenerResult = await fmp.screener.getScreener({ + sector: firstSector, + marketCapMoreThan: 1000000000, + isActivelyTrading: true, + limit: 3, + }); + + expect(screenerResult.success).toBe(true); + expect(screenerResult.data).toBeDefined(); + + if (screenerResult.data && Array.isArray(screenerResult.data)) { + screenerResult.data.forEach(company => { + expect(company.sector).toBe(firstSector); + }); + } + } + }, + API_TIMEOUT, + ); + }); +}); diff --git a/packages/api/src/__tests__/endpoints/senate-house.test.ts b/packages/api/src/__tests__/endpoints/senate-house.test.ts index 7b10981..6161ccc 100644 --- a/packages/api/src/__tests__/endpoints/senate-house.test.ts +++ b/packages/api/src/__tests__/endpoints/senate-house.test.ts @@ -1,4 +1,5 @@ import { FMP } from '../../fmp'; +import { FAST_TIMEOUT } from '../utils/test-setup'; describe('SenateHouseEndpoints', () => { let fmp: FMP; @@ -8,137 +9,165 @@ describe('SenateHouseEndpoints', () => { }); describe('getSenateTrading', () => { - it('should get senate trading data for a specific symbol', async () => { - const result = await fmp.senateHouse.getSenateTrading({ - symbol: 'AAPL', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - verify SenateTradingResponse structure - if (result.success && result.data) { - const senateData = result.data; - expect(Array.isArray(senateData)).toBe(true); - - if (senateData.length > 0) { - const firstItem = senateData[0]; - // Check for SenateTradingResponse specific fields - expect(firstItem).toHaveProperty('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('office'); - expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('transactionDate'); - expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('assetDescription'); - expect(firstItem).toHaveProperty('assetType'); - expect(firstItem).toHaveProperty('type'); - expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('comment'); - expect(firstItem).toHaveProperty('symbol'); - // Note: Both Senate and House now have similar fields in unified structure + it( + 'should get senate trading data for a specific symbol', + async () => { + const result = await fmp.senateHouse.getSenateTrading({ + symbol: 'AAPL', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + // Type safety test - verify SenateTradingResponse structure + if (result.success && result.data) { + const senateData = result.data; + expect(Array.isArray(senateData)).toBe(true); + + if (senateData.length > 0) { + const firstItem = senateData[0]; + // Check for SenateTradingResponse specific fields + expect(firstItem).toHaveProperty('firstName'); + expect(firstItem).toHaveProperty('lastName'); + expect(firstItem).toHaveProperty('office'); + expect(firstItem).toHaveProperty('disclosureDate'); + expect(firstItem).toHaveProperty('transactionDate'); + expect(firstItem).toHaveProperty('owner'); + expect(firstItem).toHaveProperty('assetDescription'); + expect(firstItem).toHaveProperty('assetType'); + expect(firstItem).toHaveProperty('type'); + expect(firstItem).toHaveProperty('amount'); + expect(firstItem).toHaveProperty('comment'); + expect(firstItem).toHaveProperty('symbol'); + // Note: Both Senate and House now have similar fields in unified structure + } } - } - }); - - it('should get senate trading data for different symbols', async () => { - const result = await fmp.senateHouse.getSenateTrading({ - symbol: 'MSFT', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }); + }, + FAST_TIMEOUT, + ); + + it( + 'should get senate trading data for different symbols', + async () => { + const result = await fmp.senateHouse.getSenateTrading({ + symbol: 'MSFT', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + }, + FAST_TIMEOUT, + ); }); describe('getSenateTradingRSSFeed', () => { - it('should get senate trading RSS feed for page 0', async () => { - const result = await fmp.senateHouse.getSenateTradingRSSFeed({ - page: 0, - limit: 5, - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - verify SenateTradingResponse structure - if (result.success && result.data) { - const senateData = result.data; - expect(Array.isArray(senateData)).toBe(true); - - if (senateData.length > 0) { - const firstItem = senateData[0]; - // Check for SenateTradingResponse specific fields - expect(firstItem).toHaveProperty('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('office'); - expect(firstItem).toHaveProperty('disclosureDate'); - // Note: Both Senate and House now have similar fields in unified structure + it( + 'should get senate trading RSS feed for page 0', + async () => { + const result = await fmp.senateHouse.getSenateTradingRSSFeed({ + page: 0, + limit: 5, + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + // Type safety test - verify SenateTradingResponse structure + if (result.success && result.data) { + const senateData = result.data; + expect(Array.isArray(senateData)).toBe(true); + + if (senateData.length > 0) { + const firstItem = senateData[0]; + // Check for SenateTradingResponse specific fields + expect(firstItem).toHaveProperty('firstName'); + expect(firstItem).toHaveProperty('lastName'); + expect(firstItem).toHaveProperty('office'); + expect(firstItem).toHaveProperty('disclosureDate'); + // Note: Both Senate and House now have similar fields in unified structure + } } - } - }); - - it('should get senate trading RSS feed for different pages', async () => { - const result = await fmp.senateHouse.getSenateTradingRSSFeed({ - page: 1, - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }); - - it('should get senate trading RSS feed for page 2', async () => { - const result = await fmp.senateHouse.getSenateTradingRSSFeed({ - page: 2, - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }); + }, + FAST_TIMEOUT, + ); + + it( + 'should get senate trading RSS feed for different pages', + async () => { + const result = await fmp.senateHouse.getSenateTradingRSSFeed({ + page: 1, + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + }, + FAST_TIMEOUT, + ); + + it( + 'should get senate trading RSS feed for page 2', + async () => { + const result = await fmp.senateHouse.getSenateTradingRSSFeed({ + page: 2, + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + }, + FAST_TIMEOUT, + ); }); describe('getHouseTrading', () => { - it('should get house trading data for a specific symbol', async () => { - const result = await fmp.senateHouse.getHouseTrading({ - symbol: 'AAPL', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - verify HouseTradingResponse structure - if (result.success && result.data) { - const houseData = result.data; - expect(Array.isArray(houseData)).toBe(true); - - if (houseData.length > 0) { - const firstItem = houseData[0]; - // Check for HouseTradingResponse specific fields - expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('transactionDate'); - expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('symbol'); - expect(firstItem).toHaveProperty('assetDescription'); - expect(firstItem).toHaveProperty('type'); - expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('district'); - expect(firstItem).toHaveProperty('link'); - expect(firstItem).toHaveProperty('capitalGainsOver200USD'); - // Note: Both Senate and House now have office field in unified structure + it( + 'should get house trading data for a specific symbol', + async () => { + const result = await fmp.senateHouse.getHouseTrading({ + symbol: 'AAPL', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + // Type safety test - verify HouseTradingResponse structure + if (result.success && result.data) { + const houseData = result.data; + expect(Array.isArray(houseData)).toBe(true); + + if (houseData.length > 0) { + const firstItem = houseData[0]; + // Check for HouseTradingResponse specific fields + expect(firstItem).toHaveProperty('disclosureDate'); + expect(firstItem).toHaveProperty('transactionDate'); + expect(firstItem).toHaveProperty('owner'); + expect(firstItem).toHaveProperty('symbol'); + expect(firstItem).toHaveProperty('assetDescription'); + expect(firstItem).toHaveProperty('type'); + expect(firstItem).toHaveProperty('amount'); + expect(firstItem).toHaveProperty('firstName'); + expect(firstItem).toHaveProperty('lastName'); + expect(firstItem).toHaveProperty('district'); + expect(firstItem).toHaveProperty('link'); + expect(firstItem).toHaveProperty('capitalGainsOver200USD'); + // Note: Both Senate and House now have office field in unified structure + } } - } - }); - - it('should get house trading data for different symbols', async () => { - const result = await fmp.senateHouse.getHouseTrading({ - symbol: 'GOOGL', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }); + }, + FAST_TIMEOUT, + ); + + it( + 'should get house trading data for different symbols', + async () => { + const result = await fmp.senateHouse.getHouseTrading({ + symbol: 'GOOGL', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + }, + FAST_TIMEOUT, + ); }); describe('getHouseTradingRSSFeed', () => { @@ -170,162 +199,194 @@ describe('SenateHouseEndpoints', () => { } }); - it('should get house trading RSS feed for different pages', async () => { - const result = await fmp.senateHouse.getHouseTradingRSSFeed({ - page: 1, - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }); - - it('should get house trading RSS feed for page 2', async () => { - const result = await fmp.senateHouse.getHouseTradingRSSFeed({ - page: 2, - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - }); + it( + 'should get house trading RSS feed for different pages', + async () => { + const result = await fmp.senateHouse.getHouseTradingRSSFeed({ + page: 1, + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + }, + FAST_TIMEOUT, + ); + + it( + 'should get house trading RSS feed for page 2', + async () => { + const result = await fmp.senateHouse.getHouseTradingRSSFeed({ + page: 2, + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + }, + FAST_TIMEOUT, + ); }); describe('getSenateTradingByName', () => { - it('should get senate trading data by name', async () => { - const result = await fmp.senateHouse.getSenateTradingByName({ - name: 'Jerry', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - verify SenateHouseTradingByNameResponse structure - if (result.success && result.data) { - const senateData = result.data; - expect(Array.isArray(senateData)).toBe(true); - - if (senateData.length > 0) { - const firstItem = senateData[0]; - // Check for SenateHouseTradingByNameResponse specific fields - expect(firstItem).toHaveProperty('symbol'); - expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('transactionDate'); - expect(firstItem).toHaveProperty('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('office'); - expect(firstItem).toHaveProperty('district'); - expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('assetDescription'); - expect(firstItem).toHaveProperty('assetType'); - expect(firstItem).toHaveProperty('type'); - expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('capitalGainsOver200USD'); - expect(firstItem).toHaveProperty('comment'); - expect(firstItem).toHaveProperty('link'); + it( + 'should get senate trading data by name', + async () => { + const result = await fmp.senateHouse.getSenateTradingByName({ + name: 'Jerry', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + // Type safety test - verify SenateHouseTradingByNameResponse structure + if (result.success && result.data) { + const senateData = result.data; + expect(Array.isArray(senateData)).toBe(true); + + if (senateData.length > 0) { + const firstItem = senateData[0]; + // Check for SenateHouseTradingByNameResponse specific fields + expect(firstItem).toHaveProperty('symbol'); + expect(firstItem).toHaveProperty('disclosureDate'); + expect(firstItem).toHaveProperty('transactionDate'); + expect(firstItem).toHaveProperty('firstName'); + expect(firstItem).toHaveProperty('lastName'); + expect(firstItem).toHaveProperty('office'); + expect(firstItem).toHaveProperty('district'); + expect(firstItem).toHaveProperty('owner'); + expect(firstItem).toHaveProperty('assetDescription'); + expect(firstItem).toHaveProperty('assetType'); + expect(firstItem).toHaveProperty('type'); + expect(firstItem).toHaveProperty('amount'); + expect(firstItem).toHaveProperty('capitalGainsOver200USD'); + expect(firstItem).toHaveProperty('comment'); + expect(firstItem).toHaveProperty('link'); + } } - } - }); - - it('should get senate trading data by different names', async () => { - const result = await fmp.senateHouse.getSenateTradingByName({ - name: 'John', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - if (result.success && result.data) { - expect(Array.isArray(result.data)).toBe(true); - if (result.data.length > 0) { - // Data found as expected - } else { - // No data found as expected + }, + FAST_TIMEOUT, + ); + + it( + 'should get senate trading data by different names', + async () => { + const result = await fmp.senateHouse.getSenateTradingByName({ + name: 'John', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + if (result.success && result.data) { + expect(Array.isArray(result.data)).toBe(true); + if (result.data.length > 0) { + // Data found as expected + } else { + // No data found as expected + } } - } - }); - - it('should handle empty results gracefully', async () => { - const result = await fmp.senateHouse.getSenateTradingByName({ - name: 'NonExistentName123', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Should return empty array, not undefined - if (result.success && result.data) { - expect(Array.isArray(result.data)).toBe(true); - expect(result.data.length).toBe(0); - } - }); + }, + FAST_TIMEOUT, + ); + + it( + 'should handle empty results gracefully', + async () => { + const result = await fmp.senateHouse.getSenateTradingByName({ + name: 'NonExistentName123', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + // Should return empty array, not undefined + if (result.success && result.data) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBe(0); + } + }, + FAST_TIMEOUT, + ); }); describe('getHouseTradingByName', () => { - it('should get house trading data by name', async () => { - const result = await fmp.senateHouse.getHouseTradingByName({ - name: 'Nancy', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Type safety test - verify SenateHouseTradingByNameResponse structure - if (result.success && result.data) { - const houseData = result.data; - expect(Array.isArray(houseData)).toBe(true); - - if (houseData.length > 0) { - const firstItem = houseData[0]; - // Check for SenateHouseTradingByNameResponse specific fields - expect(firstItem).toHaveProperty('symbol'); - expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('transactionDate'); - expect(firstItem).toHaveProperty('firstName'); - expect(firstItem).toHaveProperty('lastName'); - expect(firstItem).toHaveProperty('office'); - expect(firstItem).toHaveProperty('district'); - expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('assetDescription'); - expect(firstItem).toHaveProperty('assetType'); - expect(firstItem).toHaveProperty('type'); - expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('capitalGainsOver200USD'); - expect(firstItem).toHaveProperty('comment'); - expect(firstItem).toHaveProperty('link'); + it( + 'should get house trading data by name', + async () => { + const result = await fmp.senateHouse.getHouseTradingByName({ + name: 'Nancy', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + // Type safety test - verify SenateHouseTradingByNameResponse structure + if (result.success && result.data) { + const houseData = result.data; + expect(Array.isArray(houseData)).toBe(true); + + if (houseData.length > 0) { + const firstItem = houseData[0]; + // Check for SenateHouseTradingByNameResponse specific fields + expect(firstItem).toHaveProperty('symbol'); + expect(firstItem).toHaveProperty('disclosureDate'); + expect(firstItem).toHaveProperty('transactionDate'); + expect(firstItem).toHaveProperty('firstName'); + expect(firstItem).toHaveProperty('lastName'); + expect(firstItem).toHaveProperty('office'); + expect(firstItem).toHaveProperty('district'); + expect(firstItem).toHaveProperty('owner'); + expect(firstItem).toHaveProperty('assetDescription'); + expect(firstItem).toHaveProperty('assetType'); + expect(firstItem).toHaveProperty('type'); + expect(firstItem).toHaveProperty('amount'); + expect(firstItem).toHaveProperty('capitalGainsOver200USD'); + expect(firstItem).toHaveProperty('comment'); + expect(firstItem).toHaveProperty('link'); + } } - } - }); - - it('should get house trading data by different names', async () => { - const result = await fmp.senateHouse.getHouseTradingByName({ - name: 'Kevin', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - if (result.success && result.data) { - expect(Array.isArray(result.data)).toBe(true); - if (result.data.length > 0) { - // Data found as expected - } else { - // No data found as expected + }, + FAST_TIMEOUT, + ); + + it( + 'should get house trading data by different names', + async () => { + const result = await fmp.senateHouse.getHouseTradingByName({ + name: 'Kevin', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + if (result.success && result.data) { + expect(Array.isArray(result.data)).toBe(true); + if (result.data.length > 0) { + // Data found as expected + } else { + // No data found as expected + } } - } - }); - - it('should handle empty results gracefully', async () => { - const result = await fmp.senateHouse.getHouseTradingByName({ - name: 'NonExistentName123', - }); - - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - - // Should return empty array, not undefined - if (result.success && result.data) { - expect(Array.isArray(result.data)).toBe(true); - expect(result.data.length).toBe(0); - } - }); + }, + FAST_TIMEOUT, + ); + + it( + 'should handle empty results gracefully', + async () => { + const result = await fmp.senateHouse.getHouseTradingByName({ + name: 'NonExistentName123', + }); + + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + // Should return empty array, not undefined + if (result.success && result.data) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBe(0); + } + }, + FAST_TIMEOUT, + ); }); }); diff --git a/packages/api/src/endpoints/screener.ts b/packages/api/src/endpoints/screener.ts new file mode 100644 index 0000000..c958195 --- /dev/null +++ b/packages/api/src/endpoints/screener.ts @@ -0,0 +1,181 @@ +// Screener endpoints for FMP API + +import { FMPClient } from '@/client'; +import { + APIResponse, + Screener, + ScreenerParams, + AvailableExchanges, + AvailableSectors, + AvailableIndustries, + AvailableCountries, +} from 'fmp-node-types'; + +export class ScreenerEndpoints { + constructor(private client: FMPClient) {} + + /** + * Screen companies based on customizable financial criteria + * + * This endpoint allows you to filter and search for companies based on various financial metrics, + * market data, and company characteristics. Perfect for finding investment opportunities, + * conducting market research, or building custom stock filters. + * + * @param params - Screening criteria and filters + * @param params.marketCapMoreThan - Minimum market capitalization + * @param params.marketCapLowerThan - Maximum market capitalization + * @param params.priceMoreThan - Minimum stock price + * @param params.priceLowerThan - Maximum stock price + * @param params.betaMoreThan - Minimum beta value + * @param params.betaLowerThan - Maximum beta value + * @param params.volumeMoreThan - Minimum trading volume + * @param params.volumeLowerThan - Maximum trading volume + * @param params.dividendMoreThan - Minimum dividend yield + * @param params.dividendLowerThan - Maximum dividend yield + * @param params.isEtf - Filter for ETFs only + * @param params.isActivelyTrading - Filter for actively trading stocks + * @param params.sector - Filter by specific sector + * @param params.industry - Filter by specific industry + * @param params.country - Filter by specific country + * @param params.exchange - Filter by specific exchange + * @param params.limit - Maximum number of results to return + * + * @returns Promise resolving to an array of companies matching the screening criteria + * + * @example + * ```typescript + * // Find large-cap tech stocks + * const techStocks = await fmp.screener.getScreener({ + * marketCapMoreThan: 10000000000, // $10B+ + * sector: 'Technology', + * isActivelyTrading: true, + * limit: 50 + * }); + * + * // Find dividend-paying stocks + * const dividendStocks = await fmp.screener.getScreener({ + * dividendMoreThan: 0.03, // 3%+ dividend yield + * marketCapMoreThan: 1000000000, // $1B+ market cap + * isActivelyTrading: true + * }); + * + * // Find small-cap value stocks + * const smallCapValue = await fmp.screener.getScreener({ + * marketCapLowerThan: 2000000000, // Under $2B + * priceMoreThan: 5, // Above $5 + * betaLowerThan: 1.2, // Lower volatility + * limit: 100 + * }); + * ``` + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#search-company-screener|FMP Income Statement Documentation} + */ + async getScreener(params: ScreenerParams): Promise> { + return this.client.get(`/company-screener`, 'stable', params); + } + + /** + * Get list of available stock exchanges for screening + * + * Retrieves all supported stock exchanges that can be used as filters + * in the company screener. Useful for building dynamic filter options + * or validating exchange parameters. + * + * @returns Promise resolving to an array of available exchanges with their details + * + * @example + * ```typescript + * // Get all available exchanges + * const exchanges = await fmp.screener.getAvailableExchanges(); + * + * // Use in screener filter + * const nasdaqStocks = await fmp.screener.getScreener({ + * exchange: 'NASDAQ', + * marketCapMoreThan: 1000000000 + * }); + * ``` + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#available-exchanges|FMP Income Statement Documentation} + */ + async getAvailableExchanges(): Promise> { + return this.client.get(`/available-exchanges`, 'stable'); + } + + /** + * Get list of available sectors for screening + * + * Retrieves all supported business sectors that can be used as filters + * in the company screener. Essential for sector-based analysis and + * building comprehensive screening tools. + * + * @returns Promise resolving to an array of available sectors with their details + * + * @example + * ```typescript + * // Get all available sectors + * const sectors = await fmp.screener.getAvailableSectors(); + * + * // Screen by specific sector + * const healthcareStocks = await fmp.screener.getScreener({ + * sector: 'Healthcare', + * marketCapMoreThan: 5000000000 + * }); + * ``` + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#available-sectors|FMP Income Statement Documentation} + */ + async getAvailableSectors(): Promise> { + return this.client.get(`/available-sectors`, 'stable'); + } + + /** + * Get list of available industries for screening + * + * Retrieves all supported industries that can be used as filters + * in the company screener. Provides more granular filtering than + * sectors for detailed industry analysis. + * + * @returns Promise resolving to an array of available industries with their details + * + * @example + * ```typescript + * // Get all available industries + * const industries = await fmp.screener.getAvailableIndustries(); + * + * // Screen by specific industry + * const softwareStocks = await fmp.screener.getScreener({ + * industry: 'Software—Application', + * isActivelyTrading: true, + * limit: 25 + * }); + * ``` + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#available-industries|FMP Income Statement Documentation} + */ + async getAvailableIndustries(): Promise> { + return this.client.get(`/available-industries`, 'stable'); + } + + /** + * Get list of available countries for screening + * + * Retrieves all supported countries that can be used as filters + * in the company screener. Useful for geographic analysis and + * international market screening. + * + * @returns Promise resolving to an array of available countries with their details + * + * @example + * ```typescript + * // Get all available countries + * const countries = await fmp.screener.getAvailableCountries(); + * + * // Screen by specific country + * const usStocks = await fmp.screener.getScreener({ + * country: 'US', + * marketCapMoreThan: 1000000000, + * isActivelyTrading: true + * }); + * ``` + * @see {@link https://site.financialmodelingprep.com/developer/docs/stable#available-countries|FMP Income Statement Documentation} + */ + async getAvailableCountries(): Promise> { + return this.client.get(`/available-countries`, 'stable'); + } +} diff --git a/packages/api/src/fmp.ts b/packages/api/src/fmp.ts index fcf0470..07cea6a 100644 --- a/packages/api/src/fmp.ts +++ b/packages/api/src/fmp.ts @@ -3,20 +3,22 @@ import { FMPClient } from './client'; import { FMPConfig } from 'fmp-node-types'; import { FMPValidation } from './utils/validation'; -import { StockEndpoints } from './endpoints/stock'; -import { FinancialEndpoints } from './endpoints/financial'; -import { ETFEndpoints } from './endpoints/etf'; -import { EconomicEndpoints } from './endpoints/economic'; -import { MarketEndpoints } from './endpoints/market'; -import { ListEndpoints } from './endpoints/list'; + import { CalendarEndpoints } from './endpoints/calendar'; import { CompanyEndpoints } from './endpoints/company'; -import { QuoteEndpoints } from './endpoints/quote'; -import { SenateHouseEndpoints } from './endpoints/senate-house'; -import { InstitutionalEndpoints } from './endpoints/institutional'; +import { EconomicEndpoints } from './endpoints/economic'; +import { ETFEndpoints } from './endpoints/etf'; +import { FinancialEndpoints } from './endpoints/financial'; import { InsiderEndpoints } from './endpoints/insider'; -import { SECEndpoints } from './endpoints/sec'; +import { InstitutionalEndpoints } from './endpoints/institutional'; +import { ListEndpoints } from './endpoints/list'; +import { MarketEndpoints } from './endpoints/market'; import { MutualFundEndpoints } from './endpoints/mutual-fund'; +import { QuoteEndpoints } from './endpoints/quote'; +import { ScreenerEndpoints } from './endpoints/screener'; +import { SECEndpoints } from './endpoints/sec'; +import { SenateHouseEndpoints } from './endpoints/senate-house'; +import { StockEndpoints } from './endpoints/stock'; /** * Main FMP API client that provides access to all endpoints @@ -49,20 +51,21 @@ import { MutualFundEndpoints } from './endpoints/mutual-fund'; * ``` */ export class FMP { - public readonly stock: StockEndpoints; - public readonly financial: FinancialEndpoints; - public readonly etf: ETFEndpoints; - public readonly economic: EconomicEndpoints; - public readonly market: MarketEndpoints; - public readonly list: ListEndpoints; public readonly calendar: CalendarEndpoints; public readonly company: CompanyEndpoints; - public readonly quote: QuoteEndpoints; - public readonly senateHouse: SenateHouseEndpoints; - public readonly institutional: InstitutionalEndpoints; + public readonly economic: EconomicEndpoints; + public readonly etf: ETFEndpoints; + public readonly financial: FinancialEndpoints; public readonly insider: InsiderEndpoints; - public readonly sec: SECEndpoints; + public readonly institutional: InstitutionalEndpoints; + public readonly list: ListEndpoints; + public readonly market: MarketEndpoints; public readonly mutualFund: MutualFundEndpoints; + public readonly quote: QuoteEndpoints; + public readonly screener: ScreenerEndpoints; + public readonly sec: SECEndpoints; + public readonly senateHouse: SenateHouseEndpoints; + public readonly stock: StockEndpoints; constructor(config: FMPConfig = {}) { // Get API key from config or environment variable @@ -81,20 +84,21 @@ export class FMP { const client = new FMPClient({ ...config, apiKey }); - this.stock = new StockEndpoints(client); - this.financial = new FinancialEndpoints(client); - this.etf = new ETFEndpoints(client); - this.economic = new EconomicEndpoints(client); - this.market = new MarketEndpoints(client); - this.list = new ListEndpoints(client); this.calendar = new CalendarEndpoints(client); this.company = new CompanyEndpoints(client); - this.quote = new QuoteEndpoints(client); - this.senateHouse = new SenateHouseEndpoints(client); - this.institutional = new InstitutionalEndpoints(client); + this.economic = new EconomicEndpoints(client); + this.etf = new ETFEndpoints(client); + this.financial = new FinancialEndpoints(client); this.insider = new InsiderEndpoints(client); - this.sec = new SECEndpoints(client); + this.institutional = new InstitutionalEndpoints(client); + this.list = new ListEndpoints(client); + this.market = new MarketEndpoints(client); this.mutualFund = new MutualFundEndpoints(client); + this.quote = new QuoteEndpoints(client); + this.screener = new ScreenerEndpoints(client); + this.sec = new SECEndpoints(client); + this.senateHouse = new SenateHouseEndpoints(client); + this.stock = new StockEndpoints(client); } /** diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1ead393..224e92d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,47 +1,50 @@ // Main entry point for all FMP types // This file provides barrel exports for all type definitions +// Calendar types +export * from './calendar'; + // Common types export * from './common'; -// Quote types -export * from './quote'; - -// Stock types -export * from './stock'; - -// Financial types -export * from './financial'; - // Company types export * from './company'; +// Economic types +export * from './economic'; + // ETF types export * from './etf'; -// Mutual fund types -export * from './mutual-fund'; +// Financial types +export * from './financial'; -// Market types -export * from './market'; +// Insider types +export * from './insider'; -// Economic types -export * from './economic'; +// Institutional types +export * from './institutional'; // List types export * from './list'; -// Calendar types -export * from './calendar'; - -// Senate house types -export * from './senate-house'; +// Market types +export * from './market'; -// Institutional types -export * from './institutional'; +// Mutual fund types +export * from './mutual-fund'; -// Insider types -export * from './insider'; +// Quote types +export * from './quote'; // SEC types export * from './sec'; + +// Screener types +export * from './screener'; + +// Senate house types +export * from './senate-house'; + +// Stock types +export * from './stock'; diff --git a/packages/types/src/screener.ts b/packages/types/src/screener.ts new file mode 100644 index 0000000..cbd3229 --- /dev/null +++ b/packages/types/src/screener.ts @@ -0,0 +1,60 @@ +export interface ScreenerParams { + marketCapMoreThan?: number; + marketCapLowerThan?: number; + sector?: string; + industry?: string; + betaMoreThan?: number; + betaLowerThan?: number; + priceMoreThan?: number; + priceLowerThan?: number; + dividendMoreThan?: number; + dividendLowerThan?: number; + volumeMoreThan?: number; + volumeLowerThan?: number; + exchange?: string; + country?: string; + isEtf?: boolean; + isFund?: boolean; + isActivelyTrading?: boolean; + limit?: number; + includeAllShareClasses?: boolean; +} + +export interface Screener { + symbol: string; + companyName: string; + marketCap: number; + sector: string; + industry: string; + beta: number; + price: number; + lastAnnualDividend: number; + volume: number; + exchange: string; + exchangeShortName: string; + country: string; + isEtf: boolean; + isFund: boolean; + isActivelyTrading: boolean; +} + +export interface AvailableExchanges { + exchange: string; + name: string; + countryName: string; + countryCode: string; + symbolSuffix: string; + delay: string; +} + +export interface AvailableSectors { + sector: string; +} + +export interface AvailableIndustries { + industry: string; +} + +export interface AvailableCountries { + country: string; +} From f5b6f12c82fd3e0dca27938a6af8e8d10aeab922 Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 11 Sep 2025 12:36:56 -0400 Subject: [PATCH 20/21] reduce requests during testing --- .../src/__tests__/endpoints/financial.test.ts | 1483 ++++++++--------- .../src/__tests__/endpoints/screener.test.ts | 262 +-- 2 files changed, 797 insertions(+), 948 deletions(-) diff --git a/packages/api/src/__tests__/endpoints/financial.test.ts b/packages/api/src/__tests__/endpoints/financial.test.ts index 853bcf2..17a5e83 100644 --- a/packages/api/src/__tests__/endpoints/financial.test.ts +++ b/packages/api/src/__tests__/endpoints/financial.test.ts @@ -1,5 +1,5 @@ import { FMP } from '../../fmp'; -import { API_KEY, isCI } from '../utils/test-setup'; +import { shouldSkipTests, createTestClient, API_TIMEOUT } from '../utils/test-setup'; // Helper function to safely access data that could be an array or single object function getFirstItem(data: T | T[]): T { @@ -66,812 +66,773 @@ function validateOptionalNumericField(value: any, _fieldName: string) { } } -describe('Financial Endpoints', () => { - if (!API_KEY || isCI) { - it('should skip tests when no API key is provided or running in CI', () => { - expect(true).toBe(true); - }); - return; - } +// Test data cache to avoid duplicate API calls +interface TestDataCache { + incomeStatement?: any; + balanceSheet?: any; + cashFlowStatement?: any; + keyMetrics?: any; + financialRatios?: any; + enterpriseValue?: any; + cashflowGrowth?: any; + incomeGrowth?: any; + balanceSheetGrowth?: any; + financialGrowth?: any; + earningsHistorical?: any; + earningsSurprises?: any; +} +describe('Financial Endpoints', () => { let fmp: FMP; + let testDataCache: TestDataCache = {}; - beforeAll(() => { - if (!API_KEY) { - throw new Error('FMP_API_KEY is required for testing'); + beforeAll(async () => { + if (shouldSkipTests()) { + console.log('Skipping financial tests - no API key available'); + return; } - fmp = new FMP({ apiKey: API_KEY }); - }); + fmp = createTestClient(); + + try { + // Fetch all financial data in parallel with timeout + const [ + incomeStatement, + balanceSheet, + cashFlowStatement, + keyMetrics, + financialRatios, + enterpriseValue, + cashflowGrowth, + incomeGrowth, + balanceSheetGrowth, + financialGrowth, + earningsHistorical, + earningsSurprises, + ] = await Promise.all([ + fmp.financial.getIncomeStatement({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getBalanceSheet({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getCashFlowStatement({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getKeyMetrics({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getFinancialRatios({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getEnterpriseValue({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getCashflowGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getIncomeGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getBalanceSheetGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getFinancialGrowth({ symbol: 'AAPL', period: 'annual', limit: 2 }), + fmp.financial.getEarningsHistorical({ symbol: 'AAPL', limit: 5 }), + fmp.financial.getEarningsSurprises('AAPL'), + ]); + + testDataCache = { + incomeStatement, + balanceSheet, + cashFlowStatement, + keyMetrics, + financialRatios, + enterpriseValue, + cashflowGrowth, + incomeGrowth, + balanceSheetGrowth, + financialGrowth, + earningsHistorical, + earningsSurprises, + }; + } catch (error) { + console.warn('Failed to pre-fetch test data:', error); + // Continue with tests - they will fetch data individually if needed + } + }, API_TIMEOUT); describe('getIncomeStatement', () => { - it('should fetch annual income statement for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getIncomeStatement({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const statement = getFirstItem(result.data!); - validateFinancialStatementBase(statement, 'AAPL'); - // Stable API might return different period formats, so just check it's defined - expect(statement.period).toBeDefined(); - expect(typeof statement.period).toBe('string'); - - // Validate key financial metrics - validateNumericField(statement.revenue, 'revenue'); - validateNumericField(statement.grossProfit, 'grossProfit'); - validateNumericField(statement.operatingIncome, 'operatingIncome'); - validateNumericField(statement.netIncome, 'netIncome'); - validateNumericField(statement.eps, 'eps'); - validateNumericField(statement.epsDiluted, 'epsDiluted'); - - // Validate expenses - validateOptionalNumericField(statement.costOfRevenue, 'costOfRevenue'); - validateOptionalNumericField(statement.operatingExpenses, 'operatingExpenses'); - validateOptionalNumericField( - statement.researchAndDevelopmentExpenses, - 'researchAndDevelopmentExpenses', - ); - validateOptionalNumericField( - statement.generalAndAdministrativeExpenses, - 'generalAndAdministrativeExpenses', - ); - - // Validate shares - validateNumericField(statement.weightedAverageShsOut, 'weightedAverageShsOut'); - validateNumericField(statement.weightedAverageShsOutDil, 'weightedAverageShsOutDil'); - }, 15000); - - it('should fetch income statement for MSFT', async () => { - const result = await fmp.financial.getIncomeStatement({ - symbol: 'MSFT', - period: 'annual', - limit: 1, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const statement = getFirstItem(result.data!); - validateFinancialStatementBase(statement, 'MSFT'); - expect(statement.revenue).toBeGreaterThan(0); - expect(statement.netIncome).toBeDefined(); - expect(statement.eps).toBeDefined(); - }, 15000); - - it('should handle different limit values correctly', async () => { - const result = await fmp.financial.getIncomeStatement({ - symbol: 'AAPL', - period: 'annual', - limit: 5, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(5); - expect(result.data!.length).toBeGreaterThan(0); - }, 15000); - - it('should handle limit of 1 correctly', async () => { - const result = await fmp.financial.getIncomeStatement({ - symbol: 'AAPL', - period: 'annual', - limit: 1, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(1); - expect(result.data!.length).toBeGreaterThan(0); - }, 15000); + it( + 'should fetch annual income statement for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping income statement test - no API key available'); + return; + } + + const result = + testDataCache.incomeStatement || + (await fmp.financial.getIncomeStatement({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const statement = getFirstItem(result.data!); + validateFinancialStatementBase(statement, 'AAPL'); + // Stable API might return different period formats, so just check it's defined + expect(statement.period).toBeDefined(); + expect(typeof statement.period).toBe('string'); + + // Validate key financial metrics + validateNumericField(statement.revenue, 'revenue'); + validateNumericField(statement.grossProfit, 'grossProfit'); + validateNumericField(statement.operatingIncome, 'operatingIncome'); + validateNumericField(statement.netIncome, 'netIncome'); + validateNumericField(statement.eps, 'eps'); + validateNumericField(statement.epsDiluted, 'epsDiluted'); + + // Validate expenses + validateOptionalNumericField(statement.costOfRevenue, 'costOfRevenue'); + validateOptionalNumericField(statement.operatingExpenses, 'operatingExpenses'); + validateOptionalNumericField( + statement.researchAndDevelopmentExpenses, + 'researchAndDevelopmentExpenses', + ); + validateOptionalNumericField( + statement.generalAndAdministrativeExpenses, + 'generalAndAdministrativeExpenses', + ); + + // Validate shares + validateNumericField(statement.weightedAverageShsOut, 'weightedAverageShsOut'); + validateNumericField(statement.weightedAverageShsOutDil, 'weightedAverageShsOutDil'); + }, + API_TIMEOUT, + ); }); describe('getBalanceSheet', () => { - it('should fetch annual balance sheet for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getBalanceSheet({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const statement = getFirstItem(result.data!); - validateFinancialStatementBase(statement, 'AAPL'); - // Stable API might return different period formats, so just check it's defined - expect(statement.period).toBeDefined(); - expect(typeof statement.period).toBe('string'); - - // Validate key balance sheet items - validateNumericField(statement.totalAssets, 'totalAssets'); - validateNumericField(statement.totalLiabilities, 'totalLiabilities'); - validateNumericField(statement.totalStockholdersEquity, 'totalStockholdersEquity'); - validateNumericField(statement.totalEquity, 'totalEquity'); - - // Validate current assets - validateOptionalNumericField(statement.cashAndCashEquivalents, 'cashAndCashEquivalents'); - validateOptionalNumericField(statement.totalCurrentAssets, 'totalCurrentAssets'); - validateOptionalNumericField(statement.inventory, 'inventory'); - validateOptionalNumericField(statement.netReceivables, 'netReceivables'); - - // Validate current liabilities - validateOptionalNumericField(statement.totalCurrentLiabilities, 'totalCurrentLiabilities'); - validateOptionalNumericField(statement.accountPayables, 'accountPayables'); - validateOptionalNumericField(statement.shortTermDebt, 'shortTermDebt'); - - // Validate debt - validateOptionalNumericField(statement.totalDebt, 'totalDebt'); - validateOptionalNumericField(statement.longTermDebt, 'longTermDebt'); - validateOptionalNumericField(statement.netDebt, 'netDebt'); - - // Validate equity components - validateOptionalNumericField(statement.commonStock, 'commonStock'); - validateOptionalNumericField(statement.retainedEarnings, 'retainedEarnings'); - }, 15000); - - it('should fetch balance sheet for GOOGL', async () => { - const result = await fmp.financial.getBalanceSheet({ - symbol: 'GOOGL', - period: 'annual', - limit: 1, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const statement = getFirstItem(result.data!); - validateFinancialStatementBase(statement, 'GOOGL'); - expect(statement.totalAssets).toBeGreaterThan(0); - expect(statement.totalLiabilities).toBeDefined(); - }, 15000); + it( + 'should fetch annual balance sheet for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping balance sheet test - no API key available'); + return; + } + + const result = + testDataCache.balanceSheet || + (await fmp.financial.getBalanceSheet({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const statement = getFirstItem(result.data!); + validateFinancialStatementBase(statement, 'AAPL'); + // Stable API might return different period formats, so just check it's defined + expect(statement.period).toBeDefined(); + expect(typeof statement.period).toBe('string'); + + // Validate key balance sheet items + validateNumericField(statement.totalAssets, 'totalAssets'); + validateNumericField(statement.totalLiabilities, 'totalLiabilities'); + validateNumericField(statement.totalStockholdersEquity, 'totalStockholdersEquity'); + validateNumericField(statement.totalEquity, 'totalEquity'); + + // Validate current assets + validateOptionalNumericField(statement.cashAndCashEquivalents, 'cashAndCashEquivalents'); + validateOptionalNumericField(statement.totalCurrentAssets, 'totalCurrentAssets'); + validateOptionalNumericField(statement.inventory, 'inventory'); + validateOptionalNumericField(statement.netReceivables, 'netReceivables'); + + // Validate current liabilities + validateOptionalNumericField(statement.totalCurrentLiabilities, 'totalCurrentLiabilities'); + validateOptionalNumericField(statement.accountPayables, 'accountPayables'); + validateOptionalNumericField(statement.shortTermDebt, 'shortTermDebt'); + + // Validate debt + validateOptionalNumericField(statement.totalDebt, 'totalDebt'); + validateOptionalNumericField(statement.longTermDebt, 'longTermDebt'); + validateOptionalNumericField(statement.netDebt, 'netDebt'); + + // Validate equity components + validateOptionalNumericField(statement.commonStock, 'commonStock'); + validateOptionalNumericField(statement.retainedEarnings, 'retainedEarnings'); + }, + API_TIMEOUT, + ); }); describe('getCashFlowStatement', () => { - it('should fetch annual cash flow statement for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getCashFlowStatement({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const statement = getFirstItem(result.data!); - validateFinancialStatementBase(statement, 'AAPL'); - // Stable API might return different period formats, so just check it's defined - expect(statement.period).toBeDefined(); - expect(typeof statement.period).toBe('string'); - - // Validate key cash flow metrics - validateNumericField(statement.netIncome, 'netIncome'); - validateOptionalNumericField(statement.operatingCashFlow, 'operatingCashFlow'); - validateOptionalNumericField(statement.freeCashFlow, 'freeCashFlow'); - validateOptionalNumericField(statement.capitalExpenditure, 'capitalExpenditure'); - - // Validate operating activities - validateOptionalNumericField( - statement.netCashProvidedByOperatingActivities, - 'netCashProvidedByOperatingActivities', - ); - validateOptionalNumericField( - statement.depreciationAndAmortization, - 'depreciationAndAmortization', - ); - validateOptionalNumericField(statement.stockBasedCompensation, 'stockBasedCompensation'); - validateOptionalNumericField(statement.changeInWorkingCapital, 'changeInWorkingCapital'); - - // Validate investing activities - validateOptionalNumericField( - statement.netCashProvidedByInvestingActivities, - 'netCashProvidedByInvestingActivities', - ); - validateOptionalNumericField( - statement.investmentsInPropertyPlantAndEquipment, - 'investmentsInPropertyPlantAndEquipment', - ); - validateOptionalNumericField(statement.acquisitionsNet, 'acquisitionsNet'); - - // Validate financing activities - validateOptionalNumericField( - statement.netCashProvidedByFinancingActivities, - 'netCashProvidedByFinancingActivities', - ); - validateOptionalNumericField(statement.netDebtIssuance, 'netDebtIssuance'); - validateOptionalNumericField(statement.commonStockRepurchased, 'commonStockRepurchased'); - validateOptionalNumericField(statement.netDividendsPaid, 'netDividendsPaid'); - - // Validate cash position - validateOptionalNumericField(statement.cashAtEndOfPeriod, 'cashAtEndOfPeriod'); - validateOptionalNumericField(statement.cashAtBeginningOfPeriod, 'cashAtBeginningOfPeriod'); - validateOptionalNumericField(statement.netChangeInCash, 'netChangeInCash'); - }, 15000); - - it('should fetch cash flow statement for TSLA', async () => { - const result = await fmp.financial.getCashFlowStatement({ - symbol: 'TSLA', - period: 'annual', - limit: 1, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const statement = getFirstItem(result.data!); - validateFinancialStatementBase(statement, 'TSLA'); - expect(statement.netIncome).toBeDefined(); - expect(statement.operatingCashFlow).toBeDefined(); - }, 15000); + it( + 'should fetch annual cash flow statement for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping cash flow statement test - no API key available'); + return; + } + + const result = + testDataCache.cashFlowStatement || + (await fmp.financial.getCashFlowStatement({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const statement = getFirstItem(result.data!); + validateFinancialStatementBase(statement, 'AAPL'); + // Stable API might return different period formats, so just check it's defined + expect(statement.period).toBeDefined(); + expect(typeof statement.period).toBe('string'); + + // Validate key cash flow metrics + validateNumericField(statement.netIncome, 'netIncome'); + validateOptionalNumericField(statement.operatingCashFlow, 'operatingCashFlow'); + validateOptionalNumericField(statement.freeCashFlow, 'freeCashFlow'); + validateOptionalNumericField(statement.capitalExpenditure, 'capitalExpenditure'); + + // Validate operating activities + validateOptionalNumericField( + statement.netCashProvidedByOperatingActivities, + 'netCashProvidedByOperatingActivities', + ); + validateOptionalNumericField( + statement.depreciationAndAmortization, + 'depreciationAndAmortization', + ); + validateOptionalNumericField(statement.stockBasedCompensation, 'stockBasedCompensation'); + validateOptionalNumericField(statement.changeInWorkingCapital, 'changeInWorkingCapital'); + + // Validate investing activities + validateOptionalNumericField( + statement.netCashProvidedByInvestingActivities, + 'netCashProvidedByInvestingActivities', + ); + validateOptionalNumericField( + statement.investmentsInPropertyPlantAndEquipment, + 'investmentsInPropertyPlantAndEquipment', + ); + validateOptionalNumericField(statement.acquisitionsNet, 'acquisitionsNet'); + + // Validate financing activities + validateOptionalNumericField( + statement.netCashProvidedByFinancingActivities, + 'netCashProvidedByFinancingActivities', + ); + validateOptionalNumericField(statement.netDebtIssuance, 'netDebtIssuance'); + validateOptionalNumericField(statement.commonStockRepurchased, 'commonStockRepurchased'); + validateOptionalNumericField(statement.netDividendsPaid, 'netDividendsPaid'); + + // Validate cash position + validateOptionalNumericField(statement.cashAtEndOfPeriod, 'cashAtEndOfPeriod'); + validateOptionalNumericField(statement.cashAtBeginningOfPeriod, 'cashAtBeginningOfPeriod'); + validateOptionalNumericField(statement.netChangeInCash, 'netChangeInCash'); + }, + API_TIMEOUT, + ); }); describe('getKeyMetrics', () => { - it('should fetch annual key metrics for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getKeyMetrics({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const metrics = getFirstItem(result.data!); - expect(metrics.symbol).toBe('AAPL'); - expect(metrics.date).toBeDefined(); - // Stable API might return different period formats, so just check it's defined - expect(metrics.period).toBeDefined(); - expect(typeof metrics.period).toBe('string'); - - // Validate valuation metrics - validateNumericField(metrics.marketCap, 'marketCap'); - validateOptionalNumericField(metrics.enterpriseValue, 'enterpriseValue'); - validateOptionalNumericField(metrics.evToSales, 'evToSales'); - validateOptionalNumericField(metrics.evToOperatingCashFlow, 'evToOperatingCashFlow'); - validateOptionalNumericField(metrics.evToFreeCashFlow, 'evToFreeCashFlow'); - - // Validate ratios - validateOptionalNumericField(metrics.currentRatio, 'currentRatio'); - validateOptionalNumericField(metrics.returnOnEquity, 'returnOnEquity'); - validateOptionalNumericField(metrics.returnOnInvestedCapital, 'returnOnInvestedCapital'); - validateOptionalNumericField(metrics.earningsYield, 'earningsYield'); - validateOptionalNumericField(metrics.freeCashFlowYield, 'freeCashFlowYield'); - }, 15000); - - it('should fetch key metrics for AMZN', async () => { - const result = await fmp.financial.getKeyMetrics({ - symbol: 'AMZN', - period: 'annual', - limit: 1, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const metrics = getFirstItem(result.data!); - expect(metrics.symbol).toBe('AMZN'); - expect(metrics.marketCap).toBeGreaterThan(0); - expect(metrics.evToSales).toBeDefined(); - }, 15000); + it( + 'should fetch annual key metrics for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping key metrics test - no API key available'); + return; + } + + const result = + testDataCache.keyMetrics || + (await fmp.financial.getKeyMetrics({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const metrics = getFirstItem(result.data!); + expect(metrics.symbol).toBe('AAPL'); + expect(metrics.date).toBeDefined(); + // Stable API might return different period formats, so just check it's defined + expect(metrics.period).toBeDefined(); + expect(typeof metrics.period).toBe('string'); + + // Validate valuation metrics + validateNumericField(metrics.marketCap, 'marketCap'); + validateOptionalNumericField(metrics.enterpriseValue, 'enterpriseValue'); + validateOptionalNumericField(metrics.evToSales, 'evToSales'); + validateOptionalNumericField(metrics.evToOperatingCashFlow, 'evToOperatingCashFlow'); + validateOptionalNumericField(metrics.evToFreeCashFlow, 'evToFreeCashFlow'); + + // Validate ratios + validateOptionalNumericField(metrics.currentRatio, 'currentRatio'); + validateOptionalNumericField(metrics.returnOnEquity, 'returnOnEquity'); + validateOptionalNumericField(metrics.returnOnInvestedCapital, 'returnOnInvestedCapital'); + validateOptionalNumericField(metrics.earningsYield, 'earningsYield'); + validateOptionalNumericField(metrics.freeCashFlowYield, 'freeCashFlowYield'); + }, + API_TIMEOUT, + ); }); describe('getFinancialRatios', () => { - it('should fetch annual financial ratios for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getFinancialRatios({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const ratios = getFirstItem(result.data!); - expect(ratios.symbol).toBe('AAPL'); - expect(ratios.date).toBeDefined(); - // Stable API might return different period formats, so just check it's defined - expect(ratios.period).toBeDefined(); - expect(typeof ratios.period).toBe('string'); - - // Validate liquidity ratios - validateOptionalNumericField(ratios.currentRatio, 'currentRatio'); - validateOptionalNumericField(ratios.quickRatio, 'quickRatio'); - validateOptionalNumericField(ratios.cashRatio, 'cashRatio'); - - // Validate profitability ratios - validateOptionalNumericField(ratios.grossProfitMargin, 'grossProfitMargin'); - validateOptionalNumericField(ratios.operatingProfitMargin, 'operatingProfitMargin'); - validateOptionalNumericField(ratios.netProfitMargin, 'netProfitMargin'); - validateOptionalNumericField(ratios.ebitMargin, 'ebitMargin'); - validateOptionalNumericField(ratios.ebitdaMargin, 'ebitdaMargin'); - - // Validate leverage ratios - validateOptionalNumericField(ratios.debtToAssetsRatio, 'debtToAssetsRatio'); - validateOptionalNumericField(ratios.debtToEquityRatio, 'debtToEquityRatio'); - validateOptionalNumericField(ratios.interestCoverageRatio, 'interestCoverageRatio'); - - // Validate efficiency ratios - validateOptionalNumericField(ratios.assetTurnover, 'assetTurnover'); - validateOptionalNumericField(ratios.inventoryTurnover, 'inventoryTurnover'); - validateOptionalNumericField(ratios.receivablesTurnover, 'receivablesTurnover'); - - // Validate valuation ratios - validateOptionalNumericField(ratios.priceToEarningsRatio, 'priceToEarningsRatio'); - validateOptionalNumericField(ratios.priceToBookRatio, 'priceToBookRatio'); - validateOptionalNumericField(ratios.priceToSalesRatio, 'priceToSalesRatio'); - validateOptionalNumericField(ratios.dividendYield, 'dividendYield'); - }, 15000); - - it('should fetch financial ratios for NVDA', async () => { - const result = await fmp.financial.getFinancialRatios({ - symbol: 'NVDA', - period: 'annual', - limit: 1, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const ratios = getFirstItem(result.data!); - expect(ratios.symbol).toBe('NVDA'); - expect(ratios.currentRatio).toBeDefined(); - expect(ratios.debtToEquityRatio).toBeDefined(); - }, 15000); + it( + 'should fetch annual financial ratios for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping financial ratios test - no API key available'); + return; + } + + const result = + testDataCache.financialRatios || + (await fmp.financial.getFinancialRatios({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const ratios = getFirstItem(result.data!); + expect(ratios.symbol).toBe('AAPL'); + expect(ratios.date).toBeDefined(); + // Stable API might return different period formats, so just check it's defined + expect(ratios.period).toBeDefined(); + expect(typeof ratios.period).toBe('string'); + + // Validate liquidity ratios + validateOptionalNumericField(ratios.currentRatio, 'currentRatio'); + validateOptionalNumericField(ratios.quickRatio, 'quickRatio'); + validateOptionalNumericField(ratios.cashRatio, 'cashRatio'); + + // Validate profitability ratios + validateOptionalNumericField(ratios.grossProfitMargin, 'grossProfitMargin'); + validateOptionalNumericField(ratios.operatingProfitMargin, 'operatingProfitMargin'); + validateOptionalNumericField(ratios.netProfitMargin, 'netProfitMargin'); + validateOptionalNumericField(ratios.ebitMargin, 'ebitMargin'); + validateOptionalNumericField(ratios.ebitdaMargin, 'ebitdaMargin'); + + // Validate leverage ratios + validateOptionalNumericField(ratios.debtToAssetsRatio, 'debtToAssetsRatio'); + validateOptionalNumericField(ratios.debtToEquityRatio, 'debtToEquityRatio'); + validateOptionalNumericField(ratios.interestCoverageRatio, 'interestCoverageRatio'); + + // Validate efficiency ratios + validateOptionalNumericField(ratios.assetTurnover, 'assetTurnover'); + validateOptionalNumericField(ratios.inventoryTurnover, 'inventoryTurnover'); + validateOptionalNumericField(ratios.receivablesTurnover, 'receivablesTurnover'); + + // Validate valuation ratios + validateOptionalNumericField(ratios.priceToEarningsRatio, 'priceToEarningsRatio'); + validateOptionalNumericField(ratios.priceToBookRatio, 'priceToBookRatio'); + validateOptionalNumericField(ratios.priceToSalesRatio, 'priceToSalesRatio'); + validateOptionalNumericField(ratios.dividendYield, 'dividendYield'); + }, + API_TIMEOUT, + ); }); describe('getEnterpriseValue', () => { - it('should fetch annual enterprise value for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getEnterpriseValue({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const ev = getFirstItem(result.data!); - expect(ev.symbol).toBe('AAPL'); - expect(ev.date).toBeDefined(); - - // Validate enterprise value components - validateNumericField(ev.enterpriseValue, 'enterpriseValue'); - validateNumericField(ev.marketCapitalization, 'marketCapitalization'); - validateNumericField(ev.stockPrice, 'stockPrice'); - validateNumericField(ev.numberOfShares, 'numberOfShares'); - - // Validate enterprise value calculation components - validateOptionalNumericField(ev.minusCashAndCashEquivalents, 'minusCashAndCashEquivalents'); - validateOptionalNumericField(ev.addTotalDebt, 'addTotalDebt'); - - // Validate enterprise value calculation - if (ev.minusCashAndCashEquivalents !== null && ev.addTotalDebt !== null) { - const calculatedEV = - ev.marketCapitalization - ev.minusCashAndCashEquivalents + ev.addTotalDebt; - expect(Math.abs(ev.enterpriseValue - calculatedEV)).toBeLessThan(1000000); // Allow for rounding differences - } - }, 15000); - - it('should fetch enterprise value for META', async () => { - const result = await fmp.financial.getEnterpriseValue({ - symbol: 'META', - period: 'annual', - limit: 1, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const ev = getFirstItem(result.data!); - expect(ev.symbol).toBe('META'); - expect(ev.enterpriseValue).toBeGreaterThan(0); - expect(ev.stockPrice).toBeGreaterThan(0); - }, 15000); + it( + 'should fetch annual enterprise value for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping enterprise value test - no API key available'); + return; + } + + const result = + testDataCache.enterpriseValue || + (await fmp.financial.getEnterpriseValue({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const ev = getFirstItem(result.data!); + expect(ev.symbol).toBe('AAPL'); + expect(ev.date).toBeDefined(); + + // Validate enterprise value components + validateNumericField(ev.enterpriseValue, 'enterpriseValue'); + validateNumericField(ev.marketCapitalization, 'marketCapitalization'); + validateNumericField(ev.stockPrice, 'stockPrice'); + validateNumericField(ev.numberOfShares, 'numberOfShares'); + + // Validate enterprise value calculation components + validateOptionalNumericField(ev.minusCashAndCashEquivalents, 'minusCashAndCashEquivalents'); + validateOptionalNumericField(ev.addTotalDebt, 'addTotalDebt'); + + // Validate enterprise value calculation + if (ev.minusCashAndCashEquivalents !== null && ev.addTotalDebt !== null) { + const calculatedEV = + ev.marketCapitalization - ev.minusCashAndCashEquivalents + ev.addTotalDebt; + expect(Math.abs(ev.enterpriseValue - calculatedEV)).toBeLessThan(1000000); // Allow for rounding differences + } + }, + API_TIMEOUT, + ); }); describe('getCashflowGrowth', () => { - it('should fetch annual cashflow growth for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getCashflowGrowth({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const growth = getFirstItem(result.data!); - validateGrowthStatementBase(growth, 'AAPL'); - // Stable API might return different period formats, so just check it's defined - expect(growth.period).toBeDefined(); - expect(typeof growth.period).toBe('string'); - - // Validate key growth metrics - validateOptionalNumericField(growth.growthNetIncome, 'growthNetIncome'); - validateOptionalNumericField(growth.growthOperatingCashFlow, 'growthOperatingCashFlow'); - validateOptionalNumericField(growth.growthFreeCashFlow, 'growthFreeCashFlow'); - validateOptionalNumericField( - growth.growthDepreciationAndAmortization, - 'growthDepreciationAndAmortization', - ); - - // Validate operating activities growth - validateOptionalNumericField( - growth.growthNetCashProvidedByOperatingActivites, - 'growthNetCashProvidedByOperatingActivites', - ); - validateOptionalNumericField( - growth.growthChangeInWorkingCapital, - 'growthChangeInWorkingCapital', - ); - validateOptionalNumericField( - growth.growthStockBasedCompensation, - 'growthStockBasedCompensation', - ); - - // Validate investing activities growth - validateOptionalNumericField( - growth.growthNetCashUsedForInvestingActivites, - 'growthNetCashUsedForInvestingActivites', - ); - validateOptionalNumericField( - growth.growthInvestmentsInPropertyPlantAndEquipment, - 'growthInvestmentsInPropertyPlantAndEquipment', - ); - validateOptionalNumericField(growth.growthAcquisitionsNet, 'growthAcquisitionsNet'); - - // Validate financing activities growth - validateOptionalNumericField( - growth.growthNetCashUsedProvidedByFinancingActivities, - 'growthNetCashUsedProvidedByFinancingActivities', - ); - validateOptionalNumericField(growth.growthDebtRepayment, 'growthDebtRepayment'); - validateOptionalNumericField(growth.growthDividendsPaid, 'growthDividendsPaid'); - }, 15000); + it( + 'should fetch annual cashflow growth for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping cashflow growth test - no API key available'); + return; + } + + const result = + testDataCache.cashflowGrowth || + (await fmp.financial.getCashflowGrowth({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const growth = getFirstItem(result.data!); + validateGrowthStatementBase(growth, 'AAPL'); + // Stable API might return different period formats, so just check it's defined + expect(growth.period).toBeDefined(); + expect(typeof growth.period).toBe('string'); + + // Validate key growth metrics + validateOptionalNumericField(growth.growthNetIncome, 'growthNetIncome'); + validateOptionalNumericField(growth.growthOperatingCashFlow, 'growthOperatingCashFlow'); + validateOptionalNumericField(growth.growthFreeCashFlow, 'growthFreeCashFlow'); + validateOptionalNumericField( + growth.growthDepreciationAndAmortization, + 'growthDepreciationAndAmortization', + ); + + // Validate operating activities growth + validateOptionalNumericField( + growth.growthNetCashProvidedByOperatingActivites, + 'growthNetCashProvidedByOperatingActivites', + ); + validateOptionalNumericField( + growth.growthChangeInWorkingCapital, + 'growthChangeInWorkingCapital', + ); + validateOptionalNumericField( + growth.growthStockBasedCompensation, + 'growthStockBasedCompensation', + ); + + // Validate investing activities growth + validateOptionalNumericField( + growth.growthNetCashUsedForInvestingActivites, + 'growthNetCashUsedForInvestingActivites', + ); + validateOptionalNumericField( + growth.growthInvestmentsInPropertyPlantAndEquipment, + 'growthInvestmentsInPropertyPlantAndEquipment', + ); + validateOptionalNumericField(growth.growthAcquisitionsNet, 'growthAcquisitionsNet'); + + // Validate financing activities growth + validateOptionalNumericField( + growth.growthNetCashUsedProvidedByFinancingActivities, + 'growthNetCashUsedProvidedByFinancingActivities', + ); + validateOptionalNumericField(growth.growthDebtRepayment, 'growthDebtRepayment'); + validateOptionalNumericField(growth.growthDividendsPaid, 'growthDividendsPaid'); + }, + API_TIMEOUT, + ); }); describe('getIncomeGrowth', () => { - it('should fetch annual income growth for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getIncomeGrowth({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const growth = getFirstItem(result.data!); - validateGrowthStatementBase(growth, 'AAPL'); - // Stable API might return different period formats, so just check it's defined - expect(growth.period).toBeDefined(); - expect(typeof growth.period).toBe('string'); - - // Validate key growth metrics - validateOptionalNumericField(growth.growthRevenue, 'growthRevenue'); - validateOptionalNumericField(growth.growthNetIncome, 'growthNetIncome'); - validateOptionalNumericField(growth.growthEPS, 'growthEPS'); - validateOptionalNumericField(growth.growthGrossProfit, 'growthGrossProfit'); - validateOptionalNumericField(growth.growthOperatingIncome, 'growthOperatingIncome'); - - // Validate expense growth - validateOptionalNumericField(growth.growthCostOfRevenue, 'growthCostOfRevenue'); - validateOptionalNumericField(growth.growthOperatingExpenses, 'growthOperatingExpenses'); - validateOptionalNumericField( - growth.growthResearchAndDevelopmentExpenses, - 'growthResearchAndDevelopmentExpenses', - ); - validateOptionalNumericField( - growth.growthGeneralAndAdministrativeExpenses, - 'growthGeneralAndAdministrativeExpenses', - ); - - // Validate profitability ratios growth - validateOptionalNumericField(growth.growthGrossProfitRatio, 'growthGrossProfitRatio'); - validateOptionalNumericField(growth.growthOperatingIncome, 'growthOperatingIncome'); - validateOptionalNumericField(growth.growthNetIncome, 'growthNetIncome'); - }, 15000); + it( + 'should fetch annual income growth for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping income growth test - no API key available'); + return; + } + + const result = + testDataCache.incomeGrowth || + (await fmp.financial.getIncomeGrowth({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const growth = getFirstItem(result.data!); + validateGrowthStatementBase(growth, 'AAPL'); + // Stable API might return different period formats, so just check it's defined + expect(growth.period).toBeDefined(); + expect(typeof growth.period).toBe('string'); + + // Validate key growth metrics + validateOptionalNumericField(growth.growthRevenue, 'growthRevenue'); + validateOptionalNumericField(growth.growthNetIncome, 'growthNetIncome'); + validateOptionalNumericField(growth.growthEPS, 'growthEPS'); + validateOptionalNumericField(growth.growthGrossProfit, 'growthGrossProfit'); + validateOptionalNumericField(growth.growthOperatingIncome, 'growthOperatingIncome'); + + // Validate expense growth + validateOptionalNumericField(growth.growthCostOfRevenue, 'growthCostOfRevenue'); + validateOptionalNumericField(growth.growthOperatingExpenses, 'growthOperatingExpenses'); + validateOptionalNumericField( + growth.growthResearchAndDevelopmentExpenses, + 'growthResearchAndDevelopmentExpenses', + ); + validateOptionalNumericField( + growth.growthGeneralAndAdministrativeExpenses, + 'growthGeneralAndAdministrativeExpenses', + ); + + // Validate profitability ratios growth + validateOptionalNumericField(growth.growthGrossProfitRatio, 'growthGrossProfitRatio'); + validateOptionalNumericField(growth.growthOperatingIncome, 'growthOperatingIncome'); + validateOptionalNumericField(growth.growthNetIncome, 'growthNetIncome'); + }, + API_TIMEOUT, + ); }); describe('getBalanceSheetGrowth', () => { - it('should fetch annual balance sheet growth for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getBalanceSheetGrowth({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const growth = getFirstItem(result.data!); - validateGrowthStatementBase(growth, 'AAPL'); - // Stable API might return different period formats, so just check it's defined - expect(growth.period).toBeDefined(); - expect(typeof growth.period).toBe('string'); - - // Validate key growth metrics - validateOptionalNumericField(growth.growthTotalAssets, 'growthTotalAssets'); - validateOptionalNumericField(growth.growthTotalLiabilities, 'growthTotalLiabilities'); - validateOptionalNumericField( - growth.growthTotalStockholdersEquity, - 'growthTotalStockholdersEquity', - ); - validateOptionalNumericField( - growth.growthCashAndCashEquivalents, - 'growthCashAndCashEquivalents', - ); - validateOptionalNumericField(growth.growthTotalDebt, 'growthTotalDebt'); - - // Validate current assets growth - validateOptionalNumericField(growth.growthTotalCurrentAssets, 'growthTotalCurrentAssets'); - validateOptionalNumericField(growth.growthInventory, 'growthInventory'); - validateOptionalNumericField(growth.growthNetReceivables, 'growthNetReceivables'); - - // Validate current liabilities growth - validateOptionalNumericField( - growth.growthTotalCurrentLiabilities, - 'growthTotalCurrentLiabilities', - ); - validateOptionalNumericField(growth.growthAccountPayables, 'growthAccountPayables'); - validateOptionalNumericField(growth.growthShortTermDebt, 'growthShortTermDebt'); - - // Validate equity components growth - validateOptionalNumericField(growth.growthCommonStock, 'growthCommonStock'); - validateOptionalNumericField(growth.growthRetainedEarnings, 'growthRetainedEarnings'); - }, 15000); + it( + 'should fetch annual balance sheet growth for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping balance sheet growth test - no API key available'); + return; + } + + const result = + testDataCache.balanceSheetGrowth || + (await fmp.financial.getBalanceSheetGrowth({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const growth = getFirstItem(result.data!); + validateGrowthStatementBase(growth, 'AAPL'); + // Stable API might return different period formats, so just check it's defined + expect(growth.period).toBeDefined(); + expect(typeof growth.period).toBe('string'); + + // Validate key growth metrics + validateOptionalNumericField(growth.growthTotalAssets, 'growthTotalAssets'); + validateOptionalNumericField(growth.growthTotalLiabilities, 'growthTotalLiabilities'); + validateOptionalNumericField( + growth.growthTotalStockholdersEquity, + 'growthTotalStockholdersEquity', + ); + validateOptionalNumericField( + growth.growthCashAndCashEquivalents, + 'growthCashAndCashEquivalents', + ); + validateOptionalNumericField(growth.growthTotalDebt, 'growthTotalDebt'); + + // Validate current assets growth + validateOptionalNumericField(growth.growthTotalCurrentAssets, 'growthTotalCurrentAssets'); + validateOptionalNumericField(growth.growthInventory, 'growthInventory'); + validateOptionalNumericField(growth.growthNetReceivables, 'growthNetReceivables'); + + // Validate current liabilities growth + validateOptionalNumericField( + growth.growthTotalCurrentLiabilities, + 'growthTotalCurrentLiabilities', + ); + validateOptionalNumericField(growth.growthAccountPayables, 'growthAccountPayables'); + validateOptionalNumericField(growth.growthShortTermDebt, 'growthShortTermDebt'); + + // Validate equity components growth + validateOptionalNumericField(growth.growthCommonStock, 'growthCommonStock'); + validateOptionalNumericField(growth.growthRetainedEarnings, 'growthRetainedEarnings'); + }, + API_TIMEOUT, + ); }); describe('getFinancialGrowth', () => { - it('should fetch annual financial growth for AAPL with comprehensive validation', async () => { - const result = await fmp.financial.getFinancialGrowth({ - symbol: 'AAPL', - period: 'annual', - limit: 2, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const growth = getFirstItem(result.data!); - validateGrowthStatementBase(growth, 'AAPL'); - // Stable API might return different period formats, so just check it's defined - expect(growth.period).toBeDefined(); - expect(typeof growth.period).toBe('string'); - - // Validate key growth metrics - validateOptionalNumericField(growth.revenueGrowth, 'revenueGrowth'); - validateOptionalNumericField(growth.netIncomeGrowth, 'netIncomeGrowth'); - validateOptionalNumericField(growth.epsgrowth, 'epsgrowth'); - validateOptionalNumericField(growth.operatingCashFlowGrowth, 'operatingCashFlowGrowth'); - validateOptionalNumericField(growth.freeCashFlowGrowth, 'freeCashFlowGrowth'); - validateOptionalNumericField(growth.assetGrowth, 'assetGrowth'); - validateOptionalNumericField(growth.debtGrowth, 'debtGrowth'); - - // Validate profitability growth - validateOptionalNumericField(growth.grossProfitGrowth, 'grossProfitGrowth'); - validateOptionalNumericField(growth.operatingIncomeGrowth, 'operatingIncomeGrowth'); - - // Validate per-share growth - validateOptionalNumericField(growth.epsdilutedGrowth, 'epsdilutedGrowth'); - validateOptionalNumericField( - growth.weightedAverageSharesGrowth, - 'weightedAverageSharesGrowth', - ); - validateOptionalNumericField(growth.dividendsPerShareGrowth, 'dividendsPerShareGrowth'); - - // Validate long-term growth rates - validateOptionalNumericField(growth.tenYRevenueGrowthPerShare, 'tenYRevenueGrowthPerShare'); - validateOptionalNumericField(growth.fiveYRevenueGrowthPerShare, 'fiveYRevenueGrowthPerShare'); - validateOptionalNumericField( - growth.threeYRevenueGrowthPerShare, - 'threeYRevenueGrowthPerShare', - ); - }, 15000); + it( + 'should fetch annual financial growth for AAPL with comprehensive validation', + async () => { + if (shouldSkipTests()) { + console.log('Skipping financial growth test - no API key available'); + return; + } + + const result = + testDataCache.financialGrowth || + (await fmp.financial.getFinancialGrowth({ + symbol: 'AAPL', + period: 'annual', + limit: 2, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const growth = getFirstItem(result.data!); + validateGrowthStatementBase(growth, 'AAPL'); + // Stable API might return different period formats, so just check it's defined + expect(growth.period).toBeDefined(); + expect(typeof growth.period).toBe('string'); + + // Validate key growth metrics + validateOptionalNumericField(growth.revenueGrowth, 'revenueGrowth'); + validateOptionalNumericField(growth.netIncomeGrowth, 'netIncomeGrowth'); + validateOptionalNumericField(growth.epsgrowth, 'epsgrowth'); + validateOptionalNumericField(growth.operatingCashFlowGrowth, 'operatingCashFlowGrowth'); + validateOptionalNumericField(growth.freeCashFlowGrowth, 'freeCashFlowGrowth'); + validateOptionalNumericField(growth.assetGrowth, 'assetGrowth'); + validateOptionalNumericField(growth.debtGrowth, 'debtGrowth'); + + // Validate profitability growth + validateOptionalNumericField(growth.grossProfitGrowth, 'grossProfitGrowth'); + validateOptionalNumericField(growth.operatingIncomeGrowth, 'operatingIncomeGrowth'); + + // Validate per-share growth + validateOptionalNumericField(growth.epsdilutedGrowth, 'epsdilutedGrowth'); + validateOptionalNumericField( + growth.weightedAverageSharesGrowth, + 'weightedAverageSharesGrowth', + ); + validateOptionalNumericField(growth.dividendsPerShareGrowth, 'dividendsPerShareGrowth'); + + // Validate long-term growth rates + validateOptionalNumericField(growth.tenYRevenueGrowthPerShare, 'tenYRevenueGrowthPerShare'); + validateOptionalNumericField( + growth.fiveYRevenueGrowthPerShare, + 'fiveYRevenueGrowthPerShare', + ); + validateOptionalNumericField( + growth.threeYRevenueGrowthPerShare, + 'threeYRevenueGrowthPerShare', + ); + }, + API_TIMEOUT, + ); }); describe('getEarningsHistorical', () => { - it('should fetch earnings historical for AAPL', async () => { - const result = await fmp.financial.getEarningsHistorical({ - symbol: 'AAPL', - limit: 5, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - - const earnings = getFirstItem(result.data!); - expect(earnings.symbol).toBe('AAPL'); - expect(earnings.date).toBeDefined(); - expect(typeof earnings.date).toBe('string'); - expect(earnings.epsActual).toBeDefined(); - expect(earnings.epsEstimated).toBeDefined(); - expect(earnings.revenueActual).toBeDefined(); - expect(earnings.revenueEstimated).toBeDefined(); - expect(earnings.lastUpdated).toBeDefined(); - expect(typeof earnings.lastUpdated).toBe('string'); - }, 15000); - - it('should fetch earnings historical for MSFT', async () => { - const result = await fmp.financial.getEarningsHistorical({ - symbol: 'MSFT', - limit: 3, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeGreaterThan(0); - expect(result.data!.length).toBeLessThanOrEqual(3); - - const earnings = getFirstItem(result.data!); - expect(earnings.symbol).toBe('MSFT'); - expect(earnings.epsActual).toBeDefined(); - expect(earnings.revenueActual).toBeDefined(); - }, 15000); + it( + 'should fetch earnings historical for AAPL', + async () => { + if (shouldSkipTests()) { + console.log('Skipping earnings historical test - no API key available'); + return; + } + + const result = + testDataCache.earningsHistorical || + (await fmp.financial.getEarningsHistorical({ + symbol: 'AAPL', + limit: 5, + })); + + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data!.length).toBeGreaterThan(0); + + const earnings = getFirstItem(result.data!); + expect(earnings.symbol).toBe('AAPL'); + expect(earnings.date).toBeDefined(); + expect(typeof earnings.date).toBe('string'); + expect(earnings.epsActual).toBeDefined(); + expect(earnings.epsEstimated).toBeDefined(); + expect(earnings.revenueActual).toBeDefined(); + expect(earnings.revenueEstimated).toBeDefined(); + expect(earnings.lastUpdated).toBeDefined(); + expect(typeof earnings.lastUpdated).toBe('string'); + }, + API_TIMEOUT, + ); }); describe('getEarningsSurprises', () => { - it('should fetch earnings surprises for AAPL', async () => { - const result = await fmp.financial.getEarningsSurprises('AAPL'); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data!.length > 0) { - const surprise = getFirstItem(result.data!); - expect(surprise.symbol).toBe('AAPL'); - expect(surprise.date).toBeDefined(); - expect(typeof surprise.date).toBe('string'); - expect(surprise.actualEarningResult).toBeDefined(); - expect(typeof surprise.actualEarningResult).toBe('number'); - expect(surprise.estimatedEarning).toBeDefined(); - expect(typeof surprise.estimatedEarning).toBe('number'); - } - }, 15000); - - it('should fetch earnings surprises for TSLA', async () => { - const result = await fmp.financial.getEarningsSurprises('TSLA'); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - - if (result.data!.length > 0) { - const surprise = getFirstItem(result.data!); - expect(surprise.symbol).toBe('TSLA'); - expect(surprise.actualEarningResult).toBeDefined(); - expect(surprise.estimatedEarning).toBeDefined(); - } - }, 15000); - }); + it( + 'should fetch earnings surprises for AAPL', + async () => { + if (shouldSkipTests()) { + console.log('Skipping earnings surprises test - no API key available'); + return; + } - describe('Error handling and edge cases', () => { - it('should handle invalid symbol gracefully', async () => { - const result = await fmp.financial.getIncomeStatement({ - symbol: 'INVALID_SYMBOL_12345', - period: 'annual', - limit: 1, - }); - - // The API might return an empty array or an error response - expect(result.success).toBeDefined(); - // If it's successful but with no data, that's also acceptable - if (result.success && result.data) { - expect(Array.isArray(result.data)).toBe(true); - } - }, 15000); - - it('should handle very large limit values', async () => { - const result = await fmp.financial.getKeyMetrics({ - symbol: 'AAPL', - period: 'annual', - limit: 50, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBeLessThanOrEqual(50); - expect(result.data!.length).toBeGreaterThan(0); - }, 15000); - - it('should handle different symbols consistently', async () => { - const symbols = ['AAPL', 'MSFT', 'GOOGL']; - const results = await Promise.all( - symbols.map(symbol => - fmp.financial.getIncomeStatement({ - symbol, - period: 'annual', - limit: 1, - }), - ), - ); + const result = + testDataCache.earningsSurprises || (await fmp.financial.getEarningsSurprises('AAPL')); - results.forEach(result => { expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(Array.isArray(result.data)).toBe(true); + if (result.data!.length > 0) { - const statement = result.data![0]; - expect(statement.symbol).toBeDefined(); - // Stable API might return different period formats, so just check it's defined - expect(statement.period).toBeDefined(); - expect(typeof statement.period).toBe('string'); + const surprise = getFirstItem(result.data!); + expect(surprise.symbol).toBe('AAPL'); + expect(surprise.date).toBeDefined(); + expect(typeof surprise.date).toBe('string'); + expect(surprise.actualEarningResult).toBeDefined(); + expect(typeof surprise.actualEarningResult).toBe('number'); + expect(surprise.estimatedEarning).toBeDefined(); + expect(typeof surprise.estimatedEarning).toBe('number'); } - }); - }, 20000); - - it('should validate data consistency across multiple calls', async () => { - const result1 = await fmp.financial.getIncomeStatement({ - symbol: 'AAPL', - period: 'annual', - limit: 1, - }); - - const result2 = await fmp.financial.getIncomeStatement({ - symbol: 'AAPL', - period: 'annual', - limit: 1, - }); - - expect(result1.success).toBe(true); - expect(result2.success).toBe(true); - expect(result1.data).toBeDefined(); - expect(result2.data).toBeDefined(); - - if (result1.data!.length > 0 && result2.data!.length > 0) { - const statement1 = result1.data![0]; - const statement2 = result2.data![0]; - - // Same symbol and period should return consistent data structure - expect(statement1.symbol).toBe(statement2.symbol); - expect(statement1.period).toBeDefined(); - expect(statement2.period).toBeDefined(); - if (statement1.reportedCurrency && statement2.reportedCurrency) { - expect(statement1.reportedCurrency).toBe(statement2.reportedCurrency); + }, + API_TIMEOUT, + ); + }); + + describe('Error handling and edge cases', () => { + it( + 'should handle invalid symbol gracefully', + async () => { + if (shouldSkipTests()) { + console.log('Skipping invalid symbol test - no API key available'); + return; + } + + const result = await fmp.financial.getIncomeStatement({ + symbol: 'INVALID_SYMBOL_12345', + period: 'annual', + limit: 1, + }); + + // The API might return an empty array or an error response + expect(result.success).toBeDefined(); + // If it's successful but with no data, that's also acceptable + if (result.success && result.data) { + expect(Array.isArray(result.data)).toBe(true); } - } - }, 20000); + }, + API_TIMEOUT, + ); }); }); diff --git a/packages/api/src/__tests__/endpoints/screener.test.ts b/packages/api/src/__tests__/endpoints/screener.test.ts index 87fb79c..9be36cf 100644 --- a/packages/api/src/__tests__/endpoints/screener.test.ts +++ b/packages/api/src/__tests__/endpoints/screener.test.ts @@ -1,16 +1,58 @@ import { FMP } from '../../fmp'; import { shouldSkipTests, createTestClient, API_TIMEOUT, FAST_TIMEOUT } from '../utils/test-setup'; +// Test data cache to avoid duplicate API calls +interface TestDataCache { + screener?: any; + availableExchanges?: any; + availableSectors?: any; + availableIndustries?: any; + availableCountries?: any; +} + describe('Screener Endpoints', () => { let fmp: FMP; + let testDataCache: TestDataCache = {}; - beforeAll(() => { + beforeAll(async () => { if (shouldSkipTests()) { console.log('Skipping screener tests - no API key available'); return; } fmp = createTestClient(); - }); + + try { + // Fetch all screener data in parallel with timeout + const [ + screener, + availableExchanges, + availableSectors, + availableIndustries, + availableCountries, + ] = await Promise.all([ + fmp.screener.getScreener({ + marketCapMoreThan: 1000000000, // $1B+ + isActivelyTrading: true, + limit: 10, + }), + fmp.screener.getAvailableExchanges(), + fmp.screener.getAvailableSectors(), + fmp.screener.getAvailableIndustries(), + fmp.screener.getAvailableCountries(), + ]); + + testDataCache = { + screener, + availableExchanges, + availableSectors, + availableIndustries, + availableCountries, + }; + } catch (error) { + console.warn('Failed to pre-fetch test data:', error); + // Continue with tests - they will fetch data individually if needed + } + }, API_TIMEOUT); describe('getScreener', () => { it( @@ -20,11 +62,14 @@ describe('Screener Endpoints', () => { console.log('Skipping screener test - no API key available'); return; } - const result = await fmp.screener.getScreener({ - marketCapMoreThan: 1000000000, // $1B+ - isActivelyTrading: true, - limit: 10, - }); + + const result = + testDataCache.screener || + (await fmp.screener.getScreener({ + marketCapMoreThan: 1000000000, // $1B+ + isActivelyTrading: true, + limit: 10, + })); expect(result.success).toBe(true); expect(result.data).toBeDefined(); @@ -45,107 +90,6 @@ describe('Screener Endpoints', () => { }, API_TIMEOUT, ); - - it( - 'should fetch tech sector companies', - async () => { - if (shouldSkipTests()) { - console.log('Skipping tech sector screener test - no API key available'); - return; - } - const result = await fmp.screener.getScreener({ - sector: 'Technology', - marketCapMoreThan: 5000000000, // $5B+ - isActivelyTrading: true, - limit: 5, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - expect(result.data.length).toBeLessThanOrEqual(5); - - const company = result.data[0]; - expect(company.symbol).toBeDefined(); - expect(company.sector).toBe('Technology'); - expect(company.marketCap).toBeGreaterThan(5000000000); - } - }, - API_TIMEOUT, - ); - - it( - 'should fetch dividend-paying stocks', - async () => { - if (shouldSkipTests()) { - console.log('Skipping dividend screener test - no API key available'); - return; - } - const result = await fmp.screener.getScreener({ - dividendMoreThan: 0.02, // 2%+ dividend yield - marketCapMoreThan: 2000000000, // $2B+ - isActivelyTrading: true, - limit: 5, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - expect(result.data.length).toBeGreaterThan(0); - expect(result.data.length).toBeLessThanOrEqual(5); - - const company = result.data[0]; - expect(company.symbol).toBeDefined(); - expect(company.lastAnnualDividend).toBeGreaterThan(0.02); - expect(company.marketCap).toBeGreaterThan(2000000000); - } - }, - API_TIMEOUT, - ); - - it( - 'should handle empty results gracefully', - async () => { - if (shouldSkipTests()) { - console.log('Skipping empty results screener test - no API key available'); - return; - } - const result = await fmp.screener.getScreener({ - marketCapMoreThan: 999999999999999, // Unrealistically high market cap - isActivelyTrading: true, - limit: 10, - }); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - expect(Array.isArray(result.data)).toBe(true); - expect(result.data!.length).toBe(0); - }, - FAST_TIMEOUT, - ); - - it( - 'should handle invalid parameters gracefully', - async () => { - if (shouldSkipTests()) { - console.log('Skipping invalid parameters screener test - no API key available'); - return; - } - const result = await fmp.screener.getScreener({ - marketCapMoreThan: -1000, // Invalid negative value - isActivelyTrading: true, - limit: 5, - }); - - // Should either return empty results or handle gracefully - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - }, - FAST_TIMEOUT, - ); }); describe('getAvailableExchanges', () => { @@ -156,7 +100,9 @@ describe('Screener Endpoints', () => { console.log('Skipping available exchanges test - no API key available'); return; } - const result = await fmp.screener.getAvailableExchanges(); + + const result = + testDataCache.availableExchanges || (await fmp.screener.getAvailableExchanges()); expect(result.success).toBe(true); expect(result.data).toBeDefined(); @@ -169,25 +115,9 @@ describe('Screener Endpoints', () => { expect(exchange.name).toBeDefined(); expect(exchange.countryName).toBeDefined(); expect(exchange.countryCode).toBeDefined(); - } - }, - FAST_TIMEOUT, - ); - it( - 'should contain common exchanges', - async () => { - if (shouldSkipTests()) { - console.log('Skipping common exchanges test - no API key available'); - return; - } - const result = await fmp.screener.getAvailableExchanges(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - const exchangeNames = result.data.map(ex => ex.exchange); + // Test that common exchanges are present + const exchangeNames = result.data.map((ex: any) => ex.exchange); expect(exchangeNames).toContain('NASDAQ'); expect(exchangeNames).toContain('NYSE'); } @@ -204,7 +134,8 @@ describe('Screener Endpoints', () => { console.log('Skipping available sectors test - no API key available'); return; } - const result = await fmp.screener.getAvailableSectors(); + + const result = testDataCache.availableSectors || (await fmp.screener.getAvailableSectors()); expect(result.success).toBe(true); expect(result.data).toBeDefined(); @@ -214,25 +145,9 @@ describe('Screener Endpoints', () => { const sector = result.data[0]; expect(sector.sector).toBeDefined(); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should contain common sectors', - async () => { - if (shouldSkipTests()) { - console.log('Skipping common sectors test - no API key available'); - return; - } - const result = await fmp.screener.getAvailableSectors(); - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - const sectors = result.data.map(s => s.sector); + // Test that common sectors are present + const sectors = result.data.map((s: any) => s.sector); expect(sectors).toContain('Technology'); expect(sectors).toContain('Healthcare'); expect(sectors).toContain('Financial Services'); @@ -250,7 +165,9 @@ describe('Screener Endpoints', () => { console.log('Skipping available industries test - no API key available'); return; } - const result = await fmp.screener.getAvailableIndustries(); + + const result = + testDataCache.availableIndustries || (await fmp.screener.getAvailableIndustries()); expect(result.success).toBe(true); expect(result.data).toBeDefined(); @@ -260,26 +177,10 @@ describe('Screener Endpoints', () => { const industry = result.data[0]; expect(industry.industry).toBeDefined(); - } - }, - FAST_TIMEOUT, - ); - - it( - 'should contain tech-related industries', - async () => { - if (shouldSkipTests()) { - console.log('Skipping tech industries test - no API key available'); - return; - } - const result = await fmp.screener.getAvailableIndustries(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - if (result.data && Array.isArray(result.data)) { - const industries = result.data.map(i => i.industry); - const hasSoftwareIndustry = industries.some(industry => + // Test that tech-related industries are present + const industries = result.data.map((i: any) => i.industry); + const hasSoftwareIndustry = industries.some((industry: string) => industry.toLowerCase().includes('software'), ); expect(hasSoftwareIndustry).toBe(true); @@ -297,7 +198,9 @@ describe('Screener Endpoints', () => { console.log('Skipping available countries test - no API key available'); return; } - const result = await fmp.screener.getAvailableCountries(); + + const result = + testDataCache.availableCountries || (await fmp.screener.getAvailableCountries()); expect(result.success).toBe(true); expect(result.data).toBeDefined(); @@ -307,25 +210,9 @@ describe('Screener Endpoints', () => { const country = result.data[0]; expect(country.country).toBeDefined(); - } - }, - FAST_TIMEOUT, - ); - it( - 'should contain major countries', - async () => { - if (shouldSkipTests()) { - console.log('Skipping major countries test - no API key available'); - return; - } - const result = await fmp.screener.getAvailableCountries(); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - if (result.data && Array.isArray(result.data)) { - const countries = result.data.map(c => c.country); + // Test that major countries are present + const countries = result.data.map((c: any) => c.country); expect(countries).toContain('US'); expect(countries).toContain('CA'); } @@ -343,8 +230,9 @@ describe('Screener Endpoints', () => { return; } - // First get available sectors - const sectorsResult = await fmp.screener.getAvailableSectors(); + // Use cached sectors data + const sectorsResult = + testDataCache.availableSectors || (await fmp.screener.getAvailableSectors()); expect(sectorsResult.success).toBe(true); expect(sectorsResult.data).toBeDefined(); From a0fae8bd48e63cd6effa70daeb2f4fe9222e669e Mon Sep 17 00:00:00 2001 From: e-roy Date: Thu, 11 Sep 2025 13:20:33 -0400 Subject: [PATCH 21/21] choir: changeset for new release --- packages/api/CHANGELOG.md | 6 ++++++ packages/api/package.json | 2 +- packages/tools/CHANGELOG.md | 8 ++++++++ packages/tools/package.json | 2 +- packages/types/CHANGELOG.md | 6 ++++++ packages/types/package.json | 2 +- 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 1eaca4f..1dbbe9f 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,11 @@ # fmp-node-api +## 0.1.8 + +### Patch Changes + +- working tools without helper functions, expand financial tools, added screener in api wrapper + ## 0.1.7 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index dd1b53f..f658bd8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-api", - "version": "0.1.7", + "version": "0.1.8", "description": "A comprehensive Node.js wrapper for Financial Modeling Prep API", "disclaimer": "This package is not affiliated with, endorsed by, or sponsored by Financial Modeling Prep (FMP). This is an independent, community-developed wrapper.", "main": "./dist/index.js", diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index 99184e9..b904b7e 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,13 @@ # fmp-ai-tools +## 0.0.11 + +### Patch Changes + +- working tools without helper functions, expand financial tools, added screener in api wrapper +- Updated dependencies + - fmp-node-api@0.1.8 + ## 0.0.10 ### Patch Changes diff --git a/packages/tools/package.json b/packages/tools/package.json index 7b93f3a..73e45e9 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "fmp-ai-tools", - "version": "0.0.11-beta.10", + "version": "0.0.11", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 7119396..32b3690 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,11 @@ # fmp-node-types +## 0.1.3 + +### Patch Changes + +- working tools without helper functions, expand financial tools, added screener in api wrapper + ## 0.1.2 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index 8af1f16..5a9b7bc 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "fmp-node-types", - "version": "0.1.2", + "version": "0.1.3", "description": "Shared TypeScript types for FMP Node Wrapper ecosystem (internal package)", "main": "./dist/index.js", "module": "./dist/index.mjs",