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/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/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/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/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/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..9dd6b87 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -18,12 +18,33 @@ 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 + + + + + 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. +
+
+
{/* 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/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/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/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 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 b2f638f..2834e32 100644 --- a/apps/examples/vercel-ai/package.json +++ b/apps/examples/vercel-ai/package.json @@ -9,14 +9,14 @@ "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", "@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/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/api/scripts/test-endpoint.ts b/packages/api/scripts/test-endpoint.ts index a307668..612f345 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; @@ -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'); @@ -550,6 +569,7 @@ async function testEndpoint() { } } else { console.log('❌ Failed:'); + console.log(result); console.log(result.error); } } catch (error) { 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/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/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/api/src/__tests__/endpoints/screener.test.ts b/packages/api/src/__tests__/endpoints/screener.test.ts new file mode 100644 index 0000000..9be36cf --- /dev/null +++ b/packages/api/src/__tests__/endpoints/screener.test.ts @@ -0,0 +1,267 @@ +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(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( + 'should fetch companies with basic screening criteria', + async () => { + if (shouldSkipTests()) { + console.log('Skipping screener test - no API key available'); + return; + } + + const result = + testDataCache.screener || + (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, + ); + }); + + describe('getAvailableExchanges', () => { + it( + 'should fetch available exchanges', + async () => { + if (shouldSkipTests()) { + console.log('Skipping available exchanges test - no API key available'); + return; + } + + const result = + testDataCache.availableExchanges || (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(); + + // Test that common exchanges are present + const exchangeNames = result.data.map((ex: any) => 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 = testDataCache.availableSectors || (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(); + + // 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'); + } + }, + 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 = + testDataCache.availableIndustries || (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(); + + // 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); + } + }, + 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 = + testDataCache.availableCountries || (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(); + + // Test that major countries are present + const countries = result.data.map((c: any) => 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; + } + + // Use cached sectors data + const sectorsResult = + testDataCache.availableSectors || (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 0842631..6161ccc 100644 --- a/packages/api/src/__tests__/endpoints/senate-house.test.ts +++ b/packages/api/src/__tests__/endpoints/senate-house.test.ts @@ -1,164 +1,180 @@ import { FMP } from '../../fmp'; - -// Mock API key for testing -const API_KEY = 'testapikey123456789012345678901234567890'; +import { FAST_TIMEOUT } from '../utils/test-setup'; describe('SenateHouseEndpoints', () => { let fmp: FMP; beforeEach(() => { - fmp = new FMP({ apiKey: API_KEY }); + fmp = new FMP(); }); 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('dateRecieved'); - 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'); - // 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'); + 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, - }); - - 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('dateRecieved'); - // Should NOT have HouseTradingResponse specific fields - expect(firstItem).not.toHaveProperty('disclosureYear'); - expect(firstItem).not.toHaveProperty('representative'); - expect(firstItem).not.toHaveProperty('district'); + 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('disclosureYear'); - expect(firstItem).toHaveProperty('disclosureDate'); - expect(firstItem).toHaveProperty('transactionDate'); - expect(firstItem).toHaveProperty('owner'); - expect(firstItem).toHaveProperty('ticker'); - expect(firstItem).toHaveProperty('assetDescription'); - expect(firstItem).toHaveProperty('type'); - expect(firstItem).toHaveProperty('amount'); - expect(firstItem).toHaveProperty('representative'); - 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'); + 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', () => { 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,183 +189,204 @@ 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 } } }); - 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) { - console.log(`✅ Senate trading by name "Jerry" returned ${senateData.length} records`); - 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'); - } else { - console.log('⚠️ Senate trading by name "Jerry" returned empty array'); + 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) { - console.log(`✅ Senate trading by name "John" returned ${result.data.length} records`); - } else { - console.log('⚠️ Senate trading by name "John" returned empty array'); + }, + 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); - console.log('✅ Non-existent name correctly returned empty array'); - } - }); + }, + 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) { - console.log(`✅ House trading by name "Nancy" returned ${houseData.length} records`); - 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'); - } else { - console.log('⚠️ House trading by name "Nancy" returned empty array'); + 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) { - console.log(`✅ House trading by name "Kevin" returned ${result.data.length} records`); - } else { - console.log('⚠️ House trading by name "Kevin" returned empty array'); + }, + 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); - console.log('✅ Non-existent name correctly returned empty array'); - } - }); + }, + 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/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'); } /** 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/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/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/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/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 7b61148..73e45e9 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", "description": "AI tools for FMP Node API - compatible with Vercel AI SDK, Langchain, OpenAI, and more", "exports": { "./vercel-ai": { @@ -52,12 +52,15 @@ }, "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" + "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/__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 84d22ed..17449ac 100644 --- a/packages/tools/src/__tests__/providers/openai/index.test.ts +++ b/packages/tools/src/__tests__/providers/openai/index.test.ts @@ -1,4 +1,10 @@ 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(), +})); + import * as OpenAIProviders from '@/providers/openai'; describe('OpenAI providers index exports', () => { @@ -14,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/__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..e7bb465 100644 --- a/packages/tools/src/__tests__/utils/version-check.test.ts +++ b/packages/tools/src/__tests__/utils/version-check.test.ts @@ -2,97 +2,30 @@ 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({}); - - expect(() => versionCheckModule.checkOpenAIAgentsVersion()).not.toThrow(); - }); - - it('should throw error when @openai/agents is incompatible', () => { - mockTool.mockImplementation(() => { - throw new Error('Zod field uses .optional() without .nullable()'); - }); - - 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', - ); - }); - - it('should include error details in thrown message', () => { - const testError = new Error('Test error message'); - mockTool.mockImplementation(() => { - throw testError; - }); - - expect(() => versionCheckModule.checkOpenAIAgentsVersion()).toThrow( - 'Error details: Test error message', - ); - }); - }); - - describe('warnOpenAIAgentsVersion', () => { - it('should not warn when version is compatible', () => { - mockTool.mockReturnValue({}); + describe('error messages', () => { + it('should provide helpful error messages', async () => { + const { checkOpenAIAgentsVersion } = await import('../../utils/version-check'); - versionCheckModule.warnOpenAIAgentsVersion(); - expect(mockConsoleWarn).not.toHaveBeenCalled(); - }); - - it('should warn when version is incompatible', () => { - mockTool.mockImplementation(() => { - throw new Error('Incompatible version'); - }); - - 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', - ); - }); + const originalResolve = require.resolve; + require.resolve = jest.fn().mockImplementation(() => { + throw new Error('Cannot resolve module'); + }) as unknown as typeof require.resolve; - it('should handle non-Error objects thrown', () => { - mockTool.mockImplementation(() => { - throw 'String error'; - }); + expect(() => checkOpenAIAgentsVersion()).toThrow('npm install @openai/agents'); + expect(() => checkOpenAIAgentsVersion()).toThrow('@openai/agents package not found'); - 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', - ); + require.resolve = originalResolve; }); }); }); 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 2a7a08c..26401f9 100644 --- a/packages/tools/src/providers/vercel-ai/index.ts +++ b/packages/tools/src/providers/vercel-ai/index.ts @@ -1,4 +1,4 @@ -/* istanbul ignore file */ +import { ToolSet } from 'ai'; import { quoteTools } from './quote'; import { companyTools } from './company'; import { financialTools } from './financial'; @@ -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; @@ -44,7 +55,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, @@ -56,7 +67,7 @@ export const fmpTools = { ...marketTools, ...senateHouseTools, ...stockTools, -} as const; +}; // Re-export individual tool groups export { @@ -72,6 +83,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..6a9a060 100644 --- a/packages/tools/src/utils/aisdk-tool-wrapper.ts +++ b/packages/tools/src/utils/aisdk-tool-wrapper.ts @@ -1,26 +1,21 @@ import { z } from 'zod'; -import { tool } from 'ai'; +import { tool, ToolSet } from 'ai'; import { logApiExecutionWithTiming } from './logger'; -// Tool configuration interface for AI SDK v2 -export interface ToolConfig { +interface AISDKToolConfig { name: string; description: string; - inputSchema: T; - execute: (args: z.infer) => Promise; + inputSchema: z.ZodSchema; + execute: (input: any) => Promise; } -// AI SDK v2 compatible tool creator using the ai library's tool function -export function createTool(config: ToolConfig) { +export const createTool = (config: AISDKToolConfig) => { const { name, description, inputSchema, execute } = config; - - // Use the AI SDK's tool function to create a compatible tool return tool({ - name, description, inputSchema, - execute: async (args: z.infer) => { - return await logApiExecutionWithTiming(name, args, () => execute(args)); + execute: async (input: any) => { + return await logApiExecutionWithTiming(name, input, () => execute(input)); }, - } as any); // Use type assertion to avoid deep type inference issues -} + } as ToolSet); +}; diff --git a/packages/tools/src/utils/openai-tool-wrapper.ts b/packages/tools/src/utils/openai-tool-wrapper.ts index 3ca1eff..ac0fc7f 100644 --- a/packages/tools/src/utils/openai-tool-wrapper.ts +++ b/packages/tools/src/utils/openai-tool-wrapper.ts @@ -12,95 +12,28 @@ export interface OpenAIToolConfig> { export function createOpenAITool>(config: OpenAIToolConfig) { const { name, description, inputSchema, execute } = config; + // Create a simple JSON schema from the Zod schema 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); - } - }); - } + Object.entries(inputSchema.shape).forEach(([key, _fieldSchema]) => { + properties[key] = { type: 'string' }; + required.push(key); + }); return tool({ name, description, - parameters: inputSchema as any, + parameters: { + type: 'object', + properties, + required, + additionalProperties: false, + } as any, strict: true, - execute: async (input: unknown) => { + execute: async (args: z.TypeOf) => { try { - const validatedInput = inputSchema.parse(input); + const validatedInput = inputSchema.parse(args); return await logApiExecutionWithTiming(name, validatedInput, () => execute(validatedInput)); } catch (error) { if (error instanceof z.ZodError) { diff --git a/packages/tools/src/utils/version-check.ts b/packages/tools/src/utils/version-check.ts index 83b07c3..f99ec31 100644 --- a/packages/tools/src/utils/version-check.ts +++ b/packages/tools/src/utils/version-check.ts @@ -1,43 +1,78 @@ +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 the installed version is less than the required version + */ +function isVersionLessThan(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( - `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}`, + `@openai/agents package not found. ` + + `This package requires @openai/agents to be installed. ` + + `Please install with: npm install @openai/agents`, ); } -} -/** - * 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); + if (isVersionLessThan(installedVersion, REQUIRED_VERSION)) { + console.warn( + `Incompatible @openai/agents version detected. ` + + `Installed version: ${installedVersion}, Required: ${REQUIRED_VERSION} or higher.`, + ); } } 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", 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; +} 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 { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index add7a70..304fe9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 @@ -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': @@ -271,17 +271,14 @@ 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) fmp-node-api: specifier: workspace:* version: link:../api - zod: - specifier: ^3.25.76 - version: 3.25.76 devDependencies: '@types/jest': specifier: ^29.5.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: @@ -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 @@ -9729,11 +9538,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 +9550,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 +9680,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 +10239,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